mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-11 19:05:20 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33fa3aca99 | ||
|
|
c772fc9527 | ||
|
|
c5d2aa3aba | ||
|
|
02a05f06bd | ||
|
|
3b0f55caf4 | ||
|
|
6a3dd4f83c | ||
|
|
177cd3de81 | ||
|
|
d473c3ae88 | ||
|
|
ee2fbe5335 | ||
|
|
48abf44ceb | ||
|
|
03c7031a3c | ||
|
|
0e5de4a824 | ||
|
|
af96bd98ac | ||
|
|
08ef509e27 | ||
|
|
fbeb4f4a6c | ||
|
|
eb09a417a1 | ||
|
|
e84ddb6cfc | ||
|
|
d01ec3b310 | ||
|
|
45fac29804 | ||
|
|
72d4c8bfd4 | ||
|
|
8f23c61fee | ||
|
|
e41eed81ae | ||
|
|
58f5fe763f | ||
|
|
214241797d | ||
|
|
aedd48c9c4 | ||
|
|
90ddf0fb57 | ||
|
|
c9fc9b4b45 | ||
|
|
8fdc69421c | ||
|
|
1deb264913 | ||
|
|
1f2fd8ac02 | ||
|
|
06aa07329f | ||
|
|
4cb9f57479 | ||
|
|
c369514741 | ||
|
|
0e28e1b6c4 | ||
|
|
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 |
+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.
|
||||||
|
---
|
||||||
+73
-39
@@ -14,31 +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: wearerequired/git-mirror-action@v1
|
- uses: Yikun/hub-mirror-action@master
|
||||||
env:
|
|
||||||
SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
|
|
||||||
with:
|
with:
|
||||||
source-repo: https://github.com/whyour/qinglong.git
|
src: github/whyour
|
||||||
destination-repo: git@gitlab.com:whyour/qinglong.git
|
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: wearerequired/git-mirror-action@v1
|
- uses: Yikun/hub-mirror-action@master
|
||||||
env:
|
|
||||||
SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
|
|
||||||
with:
|
with:
|
||||||
source-repo: https://github.com/whyour/qinglong.git
|
src: github/whyour
|
||||||
destination-repo: git@gitee.com:whyour/qinglong.git
|
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
|
||||||
@@ -74,24 +78,43 @@ jobs:
|
|||||||
git config --local user.email 'github-actions[bot]@users.noreply.github.com'
|
git config --local user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
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}
|
||||||
|
|
||||||
|
static_gitlab:
|
||||||
|
needs: build-static
|
||||||
|
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
|
||||||
|
|
||||||
mkdir -p ~/.ssh
|
static_gitee:
|
||||||
echo "${PRIVATE_KEY}" > ~/.ssh/id_rsa
|
needs: build-static
|
||||||
chmod 600 ~/.ssh/id_rsa
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l git"
|
- uses: actions/checkout@v4
|
||||||
git remote add gitee "${REPO_GITEE}"
|
with:
|
||||||
git remote add gitlab "${REPO_GITLAB}"
|
fetch-depth: 0
|
||||||
git gc
|
- uses: Yikun/hub-mirror-action@master
|
||||||
git push --force --quiet gitee ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
with:
|
||||||
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
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
|
||||||
@@ -106,6 +129,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: Read version from version.yaml
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=$(grep '^version:' version.yaml | awk '{print $2}')
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
- name: Setup timezone
|
- name: Setup timezone
|
||||||
uses: szenius/set-timezone@v2.0
|
uses: szenius/set-timezone@v2.0
|
||||||
with:
|
with:
|
||||||
@@ -131,19 +161,13 @@ jobs:
|
|||||||
images: |
|
images: |
|
||||||
${{ github.repository }}
|
${{ github.repository }}
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
# generate Docker tags based on the following events/attributes
|
|
||||||
# nightly, master, pr-2, 1.2.3, 1.2, 1
|
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=false
|
latest=false
|
||||||
tags: |
|
tags: |
|
||||||
type=schedule,pattern=nightly
|
type=ref,event=branch,enable=${{ github.ref == format('refs/heads/{0}', 'develop') }}
|
||||||
type=edge
|
|
||||||
type=ref,event=pr
|
|
||||||
type=ref,event=branch,enable=${{ github.ref != format('refs/heads/{0}', 'master') }}
|
|
||||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
|
type=raw,value=${{ steps.version.outputs.version }},enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
type=semver,pattern={{major}}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
@@ -160,8 +184,8 @@ jobs:
|
|||||||
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
|
||||||
@@ -173,12 +197,12 @@ jobs:
|
|||||||
- name: Image digest
|
- name: Image digest
|
||||||
run: |
|
run: |
|
||||||
echo ${{ steps.docker_build.outputs.digest }}
|
echo ${{ steps.docker_build.outputs.digest }}
|
||||||
|
|
||||||
build310:
|
build310:
|
||||||
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
|
||||||
@@ -193,6 +217,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: Read version from version.yaml
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=$(grep '^version:' version.yaml | awk '{print $2}')
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
- name: Setup timezone
|
- name: Setup timezone
|
||||||
uses: szenius/set-timezone@v2.0
|
uses: szenius/set-timezone@v2.0
|
||||||
with:
|
with:
|
||||||
@@ -226,11 +257,14 @@ jobs:
|
|||||||
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
|
||||||
tags: whyour/qinglong:python3.10
|
tags: |
|
||||||
|
whyour/qinglong:python3.10
|
||||||
|
whyour/qinglong:${{ steps.version.outputs.version }}-python3.10
|
||||||
cache-from: type=registry,ref=whyour/qinglong:cache-python3.10
|
cache-from: type=registry,ref=whyour/qinglong:cache-python3.10
|
||||||
cache-to: type=registry,ref=whyour/qinglong:cache-python3.10,mode=max
|
cache-to: type=registry,ref=whyour/qinglong:cache-python3.10,mode=max
|
||||||
|
|
||||||
@@ -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' },
|
||||||
|
|||||||
+4
-2
@@ -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.ninesix.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.ninesix.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>
|
||||||
|
|
||||||

|

|
||||||
@@ -41,6 +41,8 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
|||||||
|
|
||||||
The `latest` image is built on `alpine` and the `debian` image is built on `debian-slim`. If you need to use a dependency that is not supported by `alpine`, it is recommended that you use the `debian` image.
|
The `latest` image is built on `alpine` and the `debian` image is built on `debian-slim`. If you need to use a dependency that is not supported by `alpine`, it is recommended that you use the `debian` image.
|
||||||
|
|
||||||
|
**⚠️ Important**: If you need to run Docker as a **non-root user**, please use the `debian` image. Alpine's `crond` requires root privileges.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull whyour/qinglong:latest
|
docker pull whyour/qinglong:latest
|
||||||
docker pull whyour/qinglong:debian
|
docker pull whyour/qinglong:debian
|
||||||
|
|||||||
@@ -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.ninesix.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.ninesix.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>
|
||||||
|
|
||||||

|

|
||||||
@@ -43,6 +43,8 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
|||||||
|
|
||||||
`latest` 镜像是基于 `alpine` 构建,`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖,建议使用 `debian` 镜像
|
`latest` 镜像是基于 `alpine` 构建,`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖,建议使用 `debian` 镜像
|
||||||
|
|
||||||
|
**⚠️ 重要提示**: 如果您需要以**非 root 用户**运行 Docker,请使用 `debian` 镜像。Alpine 的 `crond` 需要 root 权限。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull whyour/qinglong:latest
|
docker pull whyour/qinglong:latest
|
||||||
docker pull whyour/qinglong:debian
|
docker pull whyour/qinglong:debian
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import 'express';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
namespace Express {
|
|
||||||
interface Request {
|
|
||||||
platform: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+22
-17
@@ -8,17 +8,28 @@ 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(
|
||||||
const logger: Logger = Container.get('logger');
|
'/',
|
||||||
try {
|
celebrate({
|
||||||
const dependenceService = Container.get(DependenceService);
|
query:
|
||||||
const data = await dependenceService.dependencies(req.query as any);
|
Joi.object({
|
||||||
return res.send({ code: 200, data });
|
searchValue: Joi.string().optional().allow(''),
|
||||||
} catch (e) {
|
type: Joi.string().optional().allow(''),
|
||||||
logger.error('🔥 error: %o', e);
|
status: Joi.string().optional().allow(''),
|
||||||
return next(e);
|
}),
|
||||||
}
|
}),
|
||||||
});
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
const data = await dependenceService.dependencies(req.query as any);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', 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);
|
||||||
|
|||||||
+41
-7
@@ -1,12 +1,12 @@
|
|||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Joi, celebrate } from 'celebrate';
|
||||||
import { Container } from 'typedi';
|
import { NextFunction, Request, Response, Router } from 'express';
|
||||||
import EnvService from '../services/env';
|
|
||||||
import { Logger } from 'winston';
|
|
||||||
import { celebrate, Joi } from 'celebrate';
|
|
||||||
import multer from 'multer';
|
|
||||||
import config from '../config';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import multer from 'multer';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import { Logger } from 'winston';
|
||||||
|
import config from '../config';
|
||||||
import { safeJSONParse } from '../config/util';
|
import { safeJSONParse } from '../config/util';
|
||||||
|
import EnvService from '../services/env';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
@@ -196,6 +196,40 @@ export default (app: Router) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/pin',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
const data = await envService.pin(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/unpin',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.number().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const envService = Container.get(EnvService);
|
||||||
|
const data = await envService.unPin(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/upload',
|
'/upload',
|
||||||
upload.single('env'),
|
upload.single('env'),
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
+66
-23
@@ -1,10 +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 config from '../config';
|
import config from '../config';
|
||||||
import { getFileContentByName, readDirs, removeAnsi, rmPath } from '../config/util';
|
import {
|
||||||
import { join, resolve } 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'];
|
||||||
|
|
||||||
@@ -29,17 +34,16 @@ export default (app: Router) => {
|
|||||||
'/detail',
|
'/detail',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const finalPath = resolve(
|
const logService = Container.get(LogService);
|
||||||
config.logPath,
|
const finalPath = logService.checkFilePath(
|
||||||
(req.query.path as string) || '',
|
(req.query.path as string) || '',
|
||||||
(req.query.file as string) || '',
|
(req.query.file as string) || '',
|
||||||
);
|
);
|
||||||
|
if (!finalPath || blacklist.includes(req.query.path as string)) {
|
||||||
if (
|
return res.send({
|
||||||
blacklist.includes(req.query.path as string) ||
|
code: 403,
|
||||||
!finalPath.startsWith(config.logPath)
|
message: '暂无权限',
|
||||||
) {
|
});
|
||||||
return res.send({ code: 403, message: '暂无权限' });
|
|
||||||
}
|
}
|
||||||
const content = await getFileContentByName(finalPath);
|
const content = await getFileContentByName(finalPath);
|
||||||
res.send({ code: 200, data: removeAnsi(content) });
|
res.send({ code: 200, data: removeAnsi(content) });
|
||||||
@@ -53,16 +57,16 @@ export default (app: Router) => {
|
|||||||
'/:file',
|
'/:file',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const finalPath = resolve(
|
const logService = Container.get(LogService);
|
||||||
config.logPath,
|
const finalPath = logService.checkFilePath(
|
||||||
(req.query.path as string) || '',
|
(req.query.path as string) || '',
|
||||||
(req.params.file as string) || '',
|
(req.params.file as string) || '',
|
||||||
);
|
);
|
||||||
if (
|
if (!finalPath || blacklist.includes(req.query.path as string)) {
|
||||||
blacklist.includes(req.path) ||
|
return res.send({
|
||||||
!finalPath.startsWith(config.logPath)
|
code: 403,
|
||||||
) {
|
message: '暂无权限',
|
||||||
return res.send({ code: 403, message: '暂无权限' });
|
});
|
||||||
}
|
}
|
||||||
const content = await getFileContentByName(finalPath);
|
const content = await getFileContentByName(finalPath);
|
||||||
res.send({ code: 200, data: content });
|
res.send({ code: 200, data: content });
|
||||||
@@ -83,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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+125
-66
@@ -1,4 +1,4 @@
|
|||||||
import { fileExist, readDirs, readDir, rmPath } from '../config/util';
|
import { fileExist, readDirs, readDir, rmPath, IFile } 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';
|
||||||
@@ -24,55 +24,68 @@ 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(
|
||||||
const logger: Logger = Container.get('logger');
|
'/',
|
||||||
try {
|
celebrate({
|
||||||
let result = [];
|
query: Joi.object({
|
||||||
const blacklist = [
|
path: Joi.string().optional().allow(''),
|
||||||
'node_modules',
|
}),
|
||||||
'.git',
|
}),
|
||||||
'.pnpm',
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
'pnpm-lock.yaml',
|
const logger: Logger = Container.get('logger');
|
||||||
'yarn.lock',
|
try {
|
||||||
'package-lock.json',
|
let result: IFile[] = [];
|
||||||
];
|
const blacklist = [
|
||||||
if (req.query.path) {
|
'node_modules',
|
||||||
const targetPath = path.join(
|
'.git',
|
||||||
config.scriptPath,
|
'.pnpm',
|
||||||
req.query.path as string,
|
'pnpm-lock.yaml',
|
||||||
);
|
'yarn.lock',
|
||||||
result = await readDir(targetPath, config.scriptPath, blacklist);
|
'package-lock.json',
|
||||||
} else {
|
];
|
||||||
result = await readDirs(
|
if (req.query.path) {
|
||||||
config.scriptPath,
|
result = await readDir(
|
||||||
config.scriptPath,
|
req.query.path as string,
|
||||||
blacklist,
|
config.scriptPath,
|
||||||
(a, b) => {
|
blacklist,
|
||||||
if (a.type === b.type) {
|
);
|
||||||
return a.title.localeCompare(b.title);
|
} else {
|
||||||
} else {
|
result = await readDirs(
|
||||||
return a.type === 'directory' ? -1 : 1;
|
config.scriptPath,
|
||||||
}
|
config.scriptPath,
|
||||||
},
|
blacklist,
|
||||||
);
|
(a, b) => {
|
||||||
|
if (a.type === b.type) {
|
||||||
|
return a.title.localeCompare(b.title);
|
||||||
|
} else {
|
||||||
|
return a.type === 'directory' ? -1 : 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
res.send({
|
||||||
|
code: 200,
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
}
|
}
|
||||||
res.send({
|
});
|
||||||
code: 200,
|
|
||||||
data: result,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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 });
|
||||||
@@ -84,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 });
|
||||||
@@ -101,8 +122,17 @@ 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(''),
|
||||||
|
file: 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 {
|
||||||
@@ -124,8 +154,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: '暂无权限',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +176,7 @@ export default (app: Router) => {
|
|||||||
path,
|
path,
|
||||||
`${originFilename.replace(/\//g, '')}`,
|
`${originFilename.replace(/\//g, '')}`,
|
||||||
);
|
);
|
||||||
|
await fs.mkdir(path, { recursive: true });
|
||||||
const filePath = join(path, `${filename.replace(/\//g, '')}`);
|
const filePath = join(path, `${filename.replace(/\//g, '')}`);
|
||||||
const fileExists = await fileExist(filePath);
|
const fileExists = await fileExist(filePath);
|
||||||
if (fileExists) {
|
if (fileExists) {
|
||||||
@@ -175,14 +206,20 @@ 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);
|
||||||
|
const filePath = scriptService.checkFilePath(path, filename);
|
||||||
|
if (!filePath) {
|
||||||
|
return res.send({
|
||||||
|
code: 403,
|
||||||
|
message: '暂无权限',
|
||||||
|
});
|
||||||
|
}
|
||||||
await writeFileWithLock(filePath, content);
|
await writeFileWithLock(filePath, content);
|
||||||
return res.send({ code: 200 });
|
return res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -196,19 +233,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) {
|
||||||
@@ -222,24 +267,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) => {
|
||||||
return next(err);
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
@@ -260,6 +311,9 @@ 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 writeFileWithLock(filePath, content || '');
|
await writeFileWithLock(filePath, content || '');
|
||||||
@@ -285,6 +339,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`);
|
||||||
@@ -312,12 +369,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);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Container } from 'typedi';
|
|||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import SubscriptionService from '../services/subscription';
|
import SubscriptionService from '../services/subscription';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
import cron_parser from 'cron-parser';
|
import { CronExpressionParser } from 'cron-parser';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
@@ -60,7 +60,7 @@ export default (app: Router) => {
|
|||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
!req.body.schedule ||
|
!req.body.schedule ||
|
||||||
cron_parser.parseExpression(req.body.schedule).hasNext()
|
CronExpressionParser.parse(req.body.schedule).hasNext()
|
||||||
) {
|
) {
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
const data = await subscriptionService.create(req.body);
|
const data = await subscriptionService.create(req.body);
|
||||||
@@ -193,7 +193,7 @@ export default (app: Router) => {
|
|||||||
if (
|
if (
|
||||||
!req.body.schedule ||
|
!req.body.schedule ||
|
||||||
typeof req.body.schedule === 'object' ||
|
typeof req.body.schedule === 'object' ||
|
||||||
cron_parser.parseExpression(req.body.schedule).hasNext()
|
CronExpressionParser.parse(req.body.schedule).hasNext()
|
||||||
) {
|
) {
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
const data = await subscriptionService.update(req.body);
|
const data = await subscriptionService.update(req.body);
|
||||||
|
|||||||
+51
-5
@@ -14,6 +14,7 @@ import {
|
|||||||
} from '../config/util';
|
} from '../config/util';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
|
import { logStreamManager } from '../shared/logStreamManager';
|
||||||
|
|
||||||
const route = Router();
|
const route = Router();
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
@@ -273,19 +274,22 @@ 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) => {
|
||||||
|
// Close the stream after task completion
|
||||||
|
await logStreamManager.closeStream(await handleLogPath(logPath));
|
||||||
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 logStreamManager.write(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 logStreamManager.write(absolutePath, message);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -316,10 +320,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);
|
||||||
}
|
}
|
||||||
@@ -385,6 +394,7 @@ export default (app: Router) => {
|
|||||||
retries: Joi.number().optional(),
|
retries: Joi.number().optional(),
|
||||||
twoFactorActivated: Joi.boolean().optional(),
|
twoFactorActivated: Joi.boolean().optional(),
|
||||||
password: Joi.string().optional(),
|
password: Joi.string().optional(),
|
||||||
|
username: Joi.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -415,4 +425,40 @@ export default (app: Router) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/config/global-ssh-key',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
globalSshKey: Joi.string().allow('').allow(null),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.updateGlobalSshKey(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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
+4
-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, getToken } from '../config/util';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
@@ -55,7 +56,8 @@ 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);
|
||||||
await userService.logout(req.platform);
|
const token = getToken(req);
|
||||||
|
await userService.logout(req.platform, token);
|
||||||
res.send({ code: 200 });
|
res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
@@ -72,9 +74,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);
|
||||||
|
|||||||
+301
-26
@@ -1,31 +1,306 @@
|
|||||||
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/app').default({ expressApp: app });
|
|
||||||
|
|
||||||
const server = app
|
|
||||||
.listen(config.port, '0.0.0.0', () => {
|
|
||||||
Logger.debug(`✌️ 后端服务启动成功!`);
|
|
||||||
console.debug(`✌️ 后端服务启动成功!`);
|
|
||||||
process.send?.('ready');
|
|
||||||
require('./loaders/bootAfter').default();
|
|
||||||
})
|
|
||||||
.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>();
|
||||||
|
private httpWorker?: Worker;
|
||||||
|
|
||||||
|
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() {
|
||||||
|
// Fork gRPC worker first and wait for it to be ready
|
||||||
|
const grpcWorker = this.forkWorker('grpc');
|
||||||
|
|
||||||
|
// Wait for gRPC worker to signal it's ready before starting HTTP worker
|
||||||
|
this.waitForWorkerReady(grpcWorker, 30000)
|
||||||
|
.then(() => {
|
||||||
|
Logger.info('✌️ gRPC worker is ready, starting HTTP worker');
|
||||||
|
this.httpWorker = this.forkWorker('http');
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
Logger.error('✌️ Failed to wait for gRPC worker:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
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...`,
|
||||||
|
);
|
||||||
|
// If gRPC worker died, restart it and wait for it to be ready
|
||||||
|
if (metadata.serviceType === 'grpc') {
|
||||||
|
const newGrpcWorker = this.forkWorker('grpc');
|
||||||
|
this.waitForWorkerReady(newGrpcWorker, 30000)
|
||||||
|
.then(() => {
|
||||||
|
Logger.info('✌️ gRPC worker restarted and ready');
|
||||||
|
// Re-register cron jobs by notifying the HTTP worker
|
||||||
|
if (this.httpWorker) {
|
||||||
|
try {
|
||||||
|
this.httpWorker.send('reregister-crons');
|
||||||
|
Logger.info('✌️ Sent reregister-crons message to HTTP worker');
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('✌️ Failed to send reregister-crons message:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
Logger.error('✌️ Failed to restart gRPC worker:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// For HTTP worker, just restart it
|
||||||
|
const newWorker = this.forkWorker(metadata.serviceType);
|
||||||
|
this.httpWorker = newWorker;
|
||||||
|
Logger.info(`✌️ Restarted ${metadata.serviceType} worker (PID: ${newWorker.process.pid})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.workerMetadataMap.delete(worker.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setupMasterShutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private waitForWorkerReady(worker: Worker, timeoutMs: number): Promise<void> {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const messageHandler = (msg: any) => {
|
||||||
|
if (msg === 'ready') {
|
||||||
|
worker.removeListener('message', messageHandler);
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
worker.on('message', messageHandler);
|
||||||
|
|
||||||
|
// Timeout after specified milliseconds
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
worker.removeListener('message', messageHandler);
|
||||||
|
reject(new Error(`Worker failed to start within ${timeoutMs / 1000} seconds`));
|
||||||
|
}, timeoutMs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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', async (msg) => {
|
||||||
|
if (msg === 'shutdown') {
|
||||||
|
this.gracefulShutdown(serviceType);
|
||||||
|
} else if (msg === 'reregister-crons' && serviceType === 'http') {
|
||||||
|
// Re-register cron jobs when gRPC worker restarts
|
||||||
|
try {
|
||||||
|
Logger.info('✌️ Received reregister-crons message, re-registering cron jobs...');
|
||||||
|
const CronService = (await import('./services/cron')).default;
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
await cronService.autosave_crontab();
|
||||||
|
Logger.info('✌️ Cron jobs re-registered successfully');
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('✌️ Failed to re-register cron jobs:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
+56
-13
@@ -2,12 +2,60 @@ import dotenv from 'dotenv';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { createRandomString } from './share';
|
import { createRandomString } from './share';
|
||||||
|
|
||||||
|
dotenv.config({
|
||||||
|
path: path.join(__dirname, '../../.env'),
|
||||||
|
});
|
||||||
|
|
||||||
|
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, '../');
|
||||||
}
|
}
|
||||||
@@ -38,6 +86,7 @@ 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');
|
||||||
@@ -65,17 +114,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,
|
||||||
@@ -118,6 +158,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',
|
||||||
@@ -134,4 +175,6 @@ export default {
|
|||||||
sqliteFile,
|
sqliteFile,
|
||||||
sshdPath,
|
sshdPath,
|
||||||
systemLogPath,
|
systemLogPath,
|
||||||
|
dependenceCachePath,
|
||||||
|
maxTokensPerPlatform: 10, // Maximum number of concurrent sessions per platform
|
||||||
};
|
};
|
||||||
|
|||||||
+130
-98
@@ -1,16 +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 { writeFileWithLock } from '../shared/utils';
|
||||||
|
import { DependenceTypes } from '../data/dependence';
|
||||||
|
import { FormData } from 'undici';
|
||||||
|
|
||||||
export * from './share';
|
export * from './share';
|
||||||
|
|
||||||
@@ -57,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);
|
||||||
@@ -237,7 +164,7 @@ enum FileType {
|
|||||||
'file',
|
'file',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IFile {
|
export interface IFile {
|
||||||
title: string;
|
title: string;
|
||||||
key: string;
|
key: string;
|
||||||
type: 'directory' | 'file';
|
type: 'directory' | 'file';
|
||||||
@@ -305,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 {
|
|
||||||
title: file,
|
if (blacklist.includes(file) || stats.isSymbolicLink()) {
|
||||||
type: stats.isDirectory() ? 'directory' : 'file',
|
continue;
|
||||||
key,
|
}
|
||||||
parent: relativePath,
|
|
||||||
};
|
if (stats.isDirectory()) {
|
||||||
});
|
result.push({
|
||||||
return result;
|
title: file,
|
||||||
|
type: 'directory',
|
||||||
|
key,
|
||||||
|
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;
|
||||||
|
} 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> {
|
||||||
@@ -352,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) {
|
||||||
@@ -433,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)));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -462,6 +417,27 @@ export async function getPid(cmd: string) {
|
|||||||
return pid ? Number(pid) : undefined;
|
return pid ? Number(pid) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAllPids(cmd: string): Promise<number[]> {
|
||||||
|
const taskCommand = `ps -eo pid,command | grep "${cmd}" | grep -v grep | awk '{print $1}'`;
|
||||||
|
const pidsStr = await promiseExec(taskCommand);
|
||||||
|
if (!pidsStr) return [];
|
||||||
|
return pidsStr
|
||||||
|
.split('\n')
|
||||||
|
.map((p) => Number(p.trim()))
|
||||||
|
.filter((p) => !isNaN(p) && p > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function killAllTasks(cmd: string): Promise<void> {
|
||||||
|
const pids = await getAllPids(cmd);
|
||||||
|
for (const pid of pids) {
|
||||||
|
try {
|
||||||
|
await killTask(pid);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors if process already terminated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface IVersion {
|
interface IVersion {
|
||||||
version: string;
|
version: string;
|
||||||
changeLogLink: string;
|
changeLogLink: string;
|
||||||
@@ -558,3 +534,59 @@ export async function setSystemTimezone(timezone: string): Promise<boolean> {
|
|||||||
return false;
|
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';
|
||||||
|
}
|
||||||
|
|||||||
+7
-1
@@ -21,6 +21,8 @@ export class Crontab {
|
|||||||
extra_schedules?: Array<{ schedule: string }>;
|
extra_schedules?: Array<{ schedule: string }>;
|
||||||
task_before?: string;
|
task_before?: string;
|
||||||
task_after?: string;
|
task_after?: string;
|
||||||
|
log_name?: string;
|
||||||
|
allow_multiple_instances?: 1 | 0;
|
||||||
|
|
||||||
constructor(options: Crontab) {
|
constructor(options: Crontab) {
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
@@ -45,6 +47,8 @@ export class Crontab {
|
|||||||
this.extra_schedules = options.extra_schedules;
|
this.extra_schedules = options.extra_schedules;
|
||||||
this.task_before = options.task_before;
|
this.task_before = options.task_before;
|
||||||
this.task_after = options.task_after;
|
this.task_after = options.task_after;
|
||||||
|
this.log_name = options.log_name;
|
||||||
|
this.allow_multiple_instances = options.allow_multiple_instances || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +59,7 @@ export enum CrontabStatus {
|
|||||||
'disabled',
|
'disabled',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CronInstance extends Model<Crontab, Crontab>, Crontab { }
|
export interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
|
||||||
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
||||||
name: {
|
name: {
|
||||||
unique: 'compositeIndex',
|
unique: 'compositeIndex',
|
||||||
@@ -84,4 +88,6 @@ export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
|||||||
extra_schedules: DataTypes.JSON,
|
extra_schedules: DataTypes.JSON,
|
||||||
task_before: DataTypes.STRING,
|
task_before: DataTypes.STRING,
|
||||||
task_after: DataTypes.STRING,
|
task_after: DataTypes.STRING,
|
||||||
|
log_name: DataTypes.STRING,
|
||||||
|
allow_multiple_instances: DataTypes.NUMBER,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
+4
-1
@@ -1,5 +1,5 @@
|
|||||||
|
import { DataTypes, Model } from 'sequelize';
|
||||||
import { sequelize } from '.';
|
import { sequelize } from '.';
|
||||||
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
|
||||||
|
|
||||||
export class Env {
|
export class Env {
|
||||||
value?: string;
|
value?: string;
|
||||||
@@ -9,6 +9,7 @@ export class Env {
|
|||||||
position?: number;
|
position?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
remarks?: string;
|
remarks?: string;
|
||||||
|
isPinned?: 1 | 0;
|
||||||
|
|
||||||
constructor(options: Env) {
|
constructor(options: Env) {
|
||||||
this.value = options.value;
|
this.value = options.value;
|
||||||
@@ -21,6 +22,7 @@ export class Env {
|
|||||||
this.position = options.position;
|
this.position = options.position;
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
this.remarks = options.remarks || '';
|
this.remarks = options.remarks || '';
|
||||||
|
this.isPinned = options.isPinned || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,4 +44,5 @@ export const EnvModel = sequelize.define<EnvInstance>('Env', {
|
|||||||
position: DataTypes.NUMBER,
|
position: DataTypes.NUMBER,
|
||||||
name: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
name: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
||||||
remarks: DataTypes.STRING,
|
remarks: DataTypes.STRING,
|
||||||
|
isPinned: DataTypes.NUMBER,
|
||||||
});
|
});
|
||||||
|
|||||||
+6
-2
@@ -1,5 +1,3 @@
|
|||||||
import { IncomingHttpHeaders } from 'http';
|
|
||||||
|
|
||||||
export enum NotificationMode {
|
export enum NotificationMode {
|
||||||
'gotify' = 'gotify',
|
'gotify' = 'gotify',
|
||||||
'goCqHttpBot' = 'goCqHttpBot',
|
'goCqHttpBot' = 'goCqHttpBot',
|
||||||
@@ -117,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 {
|
||||||
@@ -143,12 +142,17 @@ export class WebhookNotification extends NotificationBaseInfo {
|
|||||||
|
|
||||||
export class LarkNotification extends NotificationBaseInfo {
|
export class LarkNotification extends NotificationBaseInfo {
|
||||||
public larkKey = '';
|
public larkKey = '';
|
||||||
|
public larkSecret = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NtfyNotification extends NotificationBaseInfo {
|
export class NtfyNotification extends NotificationBaseInfo {
|
||||||
public ntfyUrl = '';
|
public ntfyUrl = '';
|
||||||
public ntfyTopic = '';
|
public ntfyTopic = '';
|
||||||
public ntfyPriority = '';
|
public ntfyPriority = '';
|
||||||
|
public ntfyToken = '';
|
||||||
|
public ntfyUsername = '';
|
||||||
|
public ntfyPassword = '';
|
||||||
|
public ntfyActions = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WxPusherBotNotification extends NotificationBaseInfo {
|
export class WxPusherBotNotification extends NotificationBaseInfo {
|
||||||
|
|||||||
+15
-1
@@ -38,6 +38,7 @@ export interface SystemConfigInfo {
|
|||||||
pythonMirror?: string;
|
pythonMirror?: string;
|
||||||
linuxMirror?: string;
|
linuxMirror?: string;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
|
globalSshKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginLogInfo {
|
export interface LoginLogInfo {
|
||||||
@@ -48,6 +49,19 @@ export interface LoginLogInfo {
|
|||||||
status?: LoginStatus;
|
status?: LoginStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TokenInfo {
|
||||||
|
value: string;
|
||||||
|
timestamp: number;
|
||||||
|
ip: string;
|
||||||
|
address: string;
|
||||||
|
platform: string;
|
||||||
|
/**
|
||||||
|
* Token expiration time in seconds since Unix epoch.
|
||||||
|
* If undefined, the token uses JWT's built-in expiration.
|
||||||
|
*/
|
||||||
|
expiration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AuthInfo {
|
export interface AuthInfo {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
@@ -58,7 +72,7 @@ export interface AuthInfo {
|
|||||||
platform: string;
|
platform: string;
|
||||||
isTwoFactorChecking: boolean;
|
isTwoFactorChecking: boolean;
|
||||||
token: string;
|
token: string;
|
||||||
tokens: Record<string, string>;
|
tokens: Record<string, string | TokenInfo[]>;
|
||||||
twoFactorActivated: boolean;
|
twoFactorActivated: boolean;
|
||||||
twoFactorSecret: string;
|
twoFactorSecret: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
|
|||||||
Vendored
-7
@@ -1,7 +0,0 @@
|
|||||||
declare namespace Express {
|
|
||||||
interface Request {
|
|
||||||
platform: 'desktop' | 'mobile';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'pstree.remy';
|
|
||||||
+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');
|
|
||||||
};
|
};
|
||||||
|
|||||||
+33
-40
@@ -19,48 +19,41 @@ export default async () => {
|
|||||||
await CrontabViewModel.sync();
|
await CrontabViewModel.sync();
|
||||||
|
|
||||||
// 初始化新增字段
|
// 初始化新增字段
|
||||||
try {
|
const migrations = [
|
||||||
await sequelize.query(
|
{
|
||||||
'alter table CrontabViews add column filterRelation VARCHAR(255)',
|
table: 'CrontabViews',
|
||||||
);
|
column: 'filterRelation',
|
||||||
} catch (error) {}
|
type: 'VARCHAR(255)',
|
||||||
try {
|
},
|
||||||
await sequelize.query(
|
{ table: 'Subscriptions', column: 'proxy', type: 'VARCHAR(255)' },
|
||||||
'alter table Subscriptions add column proxy VARCHAR(255)',
|
{ table: 'CrontabViews', column: 'type', type: 'NUMBER' },
|
||||||
);
|
{ table: 'Subscriptions', column: 'autoAddCron', type: 'NUMBER' },
|
||||||
} catch (error) {}
|
{ table: 'Subscriptions', column: 'autoDelCron', type: 'NUMBER' },
|
||||||
try {
|
{ table: 'Crontabs', column: 'sub_id', type: 'NUMBER' },
|
||||||
await sequelize.query('alter table CrontabViews add column type NUMBER');
|
{ table: 'Crontabs', column: 'extra_schedules', type: 'JSON' },
|
||||||
} catch (error) {}
|
{ table: 'Crontabs', column: 'task_before', type: 'TEXT' },
|
||||||
try {
|
{ table: 'Crontabs', column: 'task_after', type: 'TEXT' },
|
||||||
await sequelize.query(
|
{ table: 'Crontabs', column: 'log_name', type: 'VARCHAR(255)' },
|
||||||
'alter table Subscriptions add column autoAddCron NUMBER',
|
{
|
||||||
);
|
table: 'Crontabs',
|
||||||
} catch (error) {}
|
column: 'allow_multiple_instances',
|
||||||
try {
|
type: 'NUMBER',
|
||||||
await sequelize.query(
|
},
|
||||||
'alter table Subscriptions add column autoDelCron NUMBER',
|
{ table: 'Envs', column: 'isPinned', type: 'NUMBER' },
|
||||||
);
|
];
|
||||||
} catch (error) {}
|
|
||||||
try {
|
for (const migration of migrations) {
|
||||||
await sequelize.query('alter table Crontabs add column sub_id NUMBER');
|
try {
|
||||||
} catch (error) {}
|
await sequelize.query(
|
||||||
try {
|
`alter table ${migration.table} add column ${migration.column} ${migration.type}`,
|
||||||
await sequelize.query(
|
);
|
||||||
'alter table Crontabs add column extra_schedules JSON',
|
} catch (error) {
|
||||||
);
|
// Column already exists or other error, continue
|
||||||
} catch (error) {}
|
}
|
||||||
try {
|
}
|
||||||
await sequelize.query('alter table Crontabs add column task_before TEXT');
|
|
||||||
} catch (error) {}
|
|
||||||
try {
|
|
||||||
await sequelize.query('alter table Crontabs add column task_after TEXT');
|
|
||||||
} catch (error) {}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+24
-6
@@ -1,8 +1,9 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
|
import os from 'os';
|
||||||
import chokidar from 'chokidar';
|
import chokidar from 'chokidar';
|
||||||
import config from '../config/index';
|
import config from '../config/index';
|
||||||
import { fileExist, promiseExec, rmPath } from '../config/util';
|
import Logger from './logger';
|
||||||
|
|
||||||
async function linkToNodeModule(src: string, dst?: string) {
|
async function linkToNodeModule(src: string, dst?: string) {
|
||||||
const target = path.join(config.rootPath, 'node_modules', dst || src);
|
const target = path.join(config.rootPath, 'node_modules', dst || src);
|
||||||
@@ -13,12 +14,22 @@ async function linkToNodeModule(src: string, dst?: string) {
|
|||||||
if (!stats) {
|
if (!stats) {
|
||||||
await fs.symlink(source, target, 'dir');
|
await fs.symlink(source, target, 'dir');
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch (error) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function linkCommand() {
|
async function linkCommand() {
|
||||||
const commandPath = await promiseExec('which node');
|
const homeDir = os.homedir();
|
||||||
const commandDir = path.dirname(commandPath);
|
let userBinDir = path.join(homeDir, 'bin');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.mkdir(userBinDir, { recursive: true });
|
||||||
|
await linkCommandToDir(userBinDir);
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('Linking command failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function linkCommandToDir(commandDir: string) {
|
||||||
const linkShell = [
|
const linkShell = [
|
||||||
{
|
{
|
||||||
src: 'update.sh',
|
src: 'update.sh',
|
||||||
@@ -36,6 +47,13 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -52,6 +70,6 @@ export default async (src: string = 'deps') => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watcher
|
watcher
|
||||||
.on('add', (path) => linkToNodeModule(src))
|
.on('add', () => linkToNodeModule(src))
|
||||||
.on('change', (path) => linkToNodeModule(src));
|
.on('change', () => linkToNodeModule(src));
|
||||||
};
|
};
|
||||||
|
|||||||
+23
-24
@@ -7,10 +7,10 @@ import { UnauthorizedError, expressjwt } from 'express-jwt';
|
|||||||
import { getPlatform, getToken } from '../config/util';
|
import { getPlatform, getToken } from '../config/util';
|
||||||
import rewrite from 'express-urlrewrite';
|
import rewrite from 'express-urlrewrite';
|
||||||
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 { IKeyvStore, shareStore } from '../shared/store';
|
||||||
|
import { isValidToken } from '../shared/auth';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
export default ({ app }: { app: Application }) => {
|
export default ({ app }: { app: Application }) => {
|
||||||
app.set('trust proxy', 'loopback');
|
app.set('trust proxy', 'loopback');
|
||||||
@@ -18,25 +18,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': '' },
|
|
||||||
logger: 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(
|
||||||
expressjwt({
|
expressjwt({
|
||||||
secret: config.secret,
|
secret: config.jwt.secret,
|
||||||
algorithms: ['HS384'],
|
algorithms: ['HS384'],
|
||||||
}).unless({
|
}).unless({
|
||||||
path: [...config.apiWhiteList, /^\/open\//],
|
path: [...config.apiWhiteList, /^\/(?!api\/).*/],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -50,7 +43,11 @@ 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 apps = await shareStore.getApps();
|
const apps = await shareStore.getApps();
|
||||||
@@ -81,11 +78,8 @@ export default ({ app }: { app: Application }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const authInfo = await shareStore.getAuthInfo();
|
const authInfo = await shareStore.getAuthInfo();
|
||||||
if (authInfo && headerToken) {
|
if (isValidToken(authInfo, headerToken, req.platform)) {
|
||||||
const { token = '', tokens = {} } = authInfo;
|
return next();
|
||||||
if (headerToken === token || tokens[req.platform] === headerToken) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorCode = headerToken ? 'invalid_token' : 'credentials_required';
|
const errorCode = headerToken ? 'invalid_token' : 'credentials_required';
|
||||||
@@ -122,10 +116,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 err: any = new Error('Not Found');
|
const indexPath = path.join(frontendPath, 'index.html');
|
||||||
err['status'] = 404;
|
res.sendFile(indexPath, (err) => {
|
||||||
next(err);
|
if (err) {
|
||||||
|
const err: any = new Error('Not Found');
|
||||||
|
err['status'] = 404;
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(errors());
|
app.use(errors());
|
||||||
|
|||||||
+20
-14
@@ -13,7 +13,7 @@ import { AuthDataType, SystemModel } from '../data/system';
|
|||||||
import SystemService from '../services/system';
|
import SystemService from '../services/system';
|
||||||
import UserService from '../services/user';
|
import UserService from '../services/user';
|
||||||
import { writeFile, readFile } from 'fs/promises';
|
import { writeFile, readFile } from 'fs/promises';
|
||||||
import { createRandomString, safeJSONParse } from '../config/util';
|
import { createRandomString, fileExist, safeJSONParse } from '../config/util';
|
||||||
import OpenService from '../services/open';
|
import OpenService from '../services/open';
|
||||||
import { shareStore } from '../shared/store';
|
import { shareStore } from '../shared/store';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
@@ -56,8 +56,11 @@ export default async () => {
|
|||||||
password: 'admin',
|
password: 'admin',
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const content = await readFile(config.authConfigFile, 'utf8');
|
const authFileExist = await fileExist(config.authConfigFile);
|
||||||
authInfo = safeJSONParse(content);
|
if (authFileExist) {
|
||||||
|
const content = await readFile(config.authConfigFile, 'utf8');
|
||||||
|
authInfo = safeJSONParse(content);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.warn('Failed to read auth config file, using default credentials');
|
Logger.warn('Failed to read auth config file, using default credentials');
|
||||||
}
|
}
|
||||||
@@ -68,24 +71,27 @@ export default async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
|
||||||
{ status: DependenceStatus.queued, log: [] },
|
|
||||||
{ where: { id: docs.map((x) => x.id!) } },
|
|
||||||
);
|
|
||||||
setTimeout(() => {
|
|
||||||
dependenceService.installDependenceOneByOne(docs);
|
|
||||||
}, 5000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await DependenceModel.update(
|
||||||
|
{ status: DependenceStatus.queued, log: [] },
|
||||||
|
{ where: { id: docs.map((x) => x.id!) } },
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
await dependenceService.installDependenceOneByOne(docs);
|
||||||
|
|
||||||
|
const bootAfterLoader = await import('./bootAfter');
|
||||||
|
bootAfterLoader.default();
|
||||||
|
}, 5000);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化更新 linux/python/nodejs 镜像源配置
|
// 初始化更新 linux/python/nodejs 镜像源配置
|
||||||
|
|||||||
@@ -116,11 +116,12 @@ export default async () => {
|
|||||||
`Neither content nor source specified for ${item.target}`,
|
`Neither content nor source specified for ${item.target}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const content = item.content || (await fs.readFile(item.source!));
|
const content =
|
||||||
|
item.content ||
|
||||||
|
(await fs.readFile(item.source!, { encoding: 'utf-8' }));
|
||||||
await writeFileWithLock(item.target, content);
|
await writeFileWithLock(item.target, content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.info('✌️ Init file down');
|
Logger.info('✌️ Init file down');
|
||||||
console.log('✌️ Init file down');
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Container } from 'typedi';
|
|||||||
import SystemService from '../services/system';
|
import SystemService from '../services/system';
|
||||||
import ScheduleService, { ScheduleTaskType } from '../services/schedule';
|
import ScheduleService, { ScheduleTaskType } from '../services/schedule';
|
||||||
import SubscriptionService from '../services/subscription';
|
import SubscriptionService from '../services/subscription';
|
||||||
|
import SshKeyService from '../services/sshKey';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { fileExist } from '../config/util';
|
import { fileExist } from '../config/util';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
@@ -10,6 +11,7 @@ export default async () => {
|
|||||||
const systemService = Container.get(SystemService);
|
const systemService = Container.get(SystemService);
|
||||||
const scheduleService = Container.get(ScheduleService);
|
const scheduleService = Container.get(ScheduleService);
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
|
const sshKeyService = Container.get(SshKeyService);
|
||||||
|
|
||||||
// 生成内置token
|
// 生成内置token
|
||||||
let tokenCommand = `ts-node-transpile-only ${join(
|
let tokenCommand = `ts-node-transpile-only ${join(
|
||||||
@@ -57,6 +59,11 @@ export default async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
systemService.updateTimezone(data.info);
|
systemService.updateTimezone(data.info);
|
||||||
|
|
||||||
|
// Apply global SSH key if configured
|
||||||
|
if (data.info.globalSshKey) {
|
||||||
|
await sshKeyService.addGlobalSSHKey(data.info.globalSshKey, 'global');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await subscriptionService.setSshConfig();
|
await subscriptionService.setSshConfig();
|
||||||
|
|||||||
+50
-25
@@ -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: '\ue6f5',
|
info: 'ℹ️', // info图标
|
||||||
warn: '\ue880',
|
warn: '⚠️', // 警告图标
|
||||||
error: '\ue602',
|
error: '❌', // 错误图标
|
||||||
debug: '\ue67f'
|
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 baseFormat = [
|
||||||
|
winston.format.splat(),
|
||||||
|
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||||
|
winston.format.align(),
|
||||||
|
];
|
||||||
|
|
||||||
|
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'),
|
||||||
|
datePattern: 'YYYY-MM-DD',
|
||||||
|
maxSize: '20m',
|
||||||
|
maxFiles: '7d',
|
||||||
|
format: plainFormat,
|
||||||
|
level: config.logs.level || 'info',
|
||||||
|
});
|
||||||
|
|
||||||
const LoggerInstance = winston.createLogger({
|
const LoggerInstance = winston.createLogger({
|
||||||
level: config.logs.level,
|
level: 'debug',
|
||||||
levels: winston.config.npm.levels,
|
levels: winston.config.npm.levels,
|
||||||
transports: [
|
transports: [consoleTransport, fileTransport],
|
||||||
new winston.transports.DailyRotateFile({
|
exceptionHandlers: [consoleTransport, fileTransport],
|
||||||
filename: path.join(config.systemLogPath, '%DATE%.log'),
|
rejectionHandlers: [consoleTransport, fileTransport],
|
||||||
...defaultOptions,
|
});
|
||||||
})
|
|
||||||
],
|
LoggerInstance.on('error', (error) => {
|
||||||
|
console.error('Logger error:', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default LoggerInstance;
|
export default LoggerInstance;
|
||||||
|
|||||||
+11
-12
@@ -4,6 +4,7 @@ import { Container } from 'typedi';
|
|||||||
import SockService from '../services/sock';
|
import SockService from '../services/sock';
|
||||||
import { getPlatform } from '../config/util';
|
import { getPlatform } from '../config/util';
|
||||||
import { shareStore } from '../shared/store';
|
import { shareStore } from '../shared/store';
|
||||||
|
import { isValidToken } from '../shared/auth';
|
||||||
|
|
||||||
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: () => {} });
|
||||||
@@ -17,21 +18,19 @@ export default async ({ server }: { server: Server }) => {
|
|||||||
const authInfo = await shareStore.getAuthInfo();
|
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 (authInfo) {
|
|
||||||
const { token = '', tokens = {} } = authInfo;
|
|
||||||
if (headerToken === token || tokens[platform] === headerToken) {
|
|
||||||
sockService.addClient(conn);
|
|
||||||
|
|
||||||
conn.on('data', (message) => {
|
if (isValidToken(authInfo, headerToken, platform)) {
|
||||||
conn.write(message);
|
sockService.addClient(conn);
|
||||||
});
|
|
||||||
|
|
||||||
conn.on('close', function () {
|
conn.on('data', (message) => {
|
||||||
sockService.removeClient(conn);
|
conn.write(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
conn.on('close', function () {
|
||||||
}
|
sockService.removeClient(conn);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.close('404');
|
conn.close('404');
|
||||||
|
|||||||
@@ -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 { expressjwt } 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(
|
|
||||||
expressjwt({
|
|
||||||
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),
|
||||||
|
};
|
||||||
|
};
|
||||||
+140
-8
@@ -53,14 +53,7 @@ message Response {
|
|||||||
optional string message = 2;
|
optional string message = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SystemNotifyRequest {
|
message ExtraScheduleItem { string schedule = 1; }
|
||||||
string title = 1;
|
|
||||||
string content = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ExtraScheduleItem {
|
|
||||||
string schedule = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message CronItem {
|
message CronItem {
|
||||||
optional int32 id = 1;
|
optional int32 id = 1;
|
||||||
@@ -104,6 +97,18 @@ message UpdateCronRequest {
|
|||||||
|
|
||||||
message DeleteCronsRequest { repeated int32 ids = 1; }
|
message DeleteCronsRequest { repeated int32 ids = 1; }
|
||||||
|
|
||||||
|
message GetCronsRequest {
|
||||||
|
optional string searchValue = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetCronByIdRequest { int32 id = 1; }
|
||||||
|
|
||||||
|
message EnableCronsRequest { repeated int32 ids = 1; }
|
||||||
|
|
||||||
|
message DisableCronsRequest { repeated int32 ids = 1; }
|
||||||
|
|
||||||
|
message RunCronsRequest { repeated int32 ids = 1; }
|
||||||
|
|
||||||
message CronsResponse {
|
message CronsResponse {
|
||||||
int32 code = 1;
|
int32 code = 1;
|
||||||
repeated CronItem data = 2;
|
repeated CronItem data = 2;
|
||||||
@@ -124,6 +129,128 @@ message CronDetailResponse {
|
|||||||
optional string message = 3;
|
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 {
|
service Api {
|
||||||
rpc GetEnvs(GetEnvsRequest) returns (EnvsResponse) {}
|
rpc GetEnvs(GetEnvsRequest) returns (EnvsResponse) {}
|
||||||
rpc CreateEnv(CreateEnvRequest) returns (EnvsResponse) {}
|
rpc CreateEnv(CreateEnvRequest) returns (EnvsResponse) {}
|
||||||
@@ -139,4 +266,9 @@ service Api {
|
|||||||
rpc CreateCron(CreateCronRequest) returns (CronResponse) {}
|
rpc CreateCron(CreateCronRequest) returns (CronResponse) {}
|
||||||
rpc UpdateCron(UpdateCronRequest) returns (CronResponse) {}
|
rpc UpdateCron(UpdateCronRequest) returns (CronResponse) {}
|
||||||
rpc DeleteCrons(DeleteCronsRequest) returns (Response) {}
|
rpc DeleteCrons(DeleteCronsRequest) returns (Response) {}
|
||||||
|
rpc GetCrons(GetCronsRequest) returns (CronsResponse) {}
|
||||||
|
rpc GetCronById(GetCronByIdRequest) returns (CronResponse) {}
|
||||||
|
rpc EnableCrons(EnableCronsRequest) returns (Response) {}
|
||||||
|
rpc DisableCrons(DisableCronsRequest) returns (Response) {}
|
||||||
|
rpc RunCrons(RunCronsRequest) returns (Response) {}
|
||||||
}
|
}
|
||||||
+2002
-81
File diff suppressed because it is too large
Load Diff
@@ -1,35 +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/db').default();
|
|
||||||
|
|
||||||
Logger.debug(`✌️ 公共服务启动成功!`);
|
|
||||||
console.debug(`✌️ 公共服务启动成功!`);
|
|
||||||
process.send?.('ready');
|
|
||||||
})
|
|
||||||
.on('error', (err) => {
|
|
||||||
Logger.error(err);
|
|
||||||
console.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -16,7 +16,7 @@ const addCron = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
Logger.info(
|
Logger.info(
|
||||||
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
'[schedule][创建定时任务] 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
schedule,
|
schedule,
|
||||||
@@ -26,7 +26,7 @@ const addCron = (
|
|||||||
if (extra_schedules?.length) {
|
if (extra_schedules?.length) {
|
||||||
extra_schedules.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,
|
||||||
|
|||||||
+178
-9
@@ -30,7 +30,14 @@ import {
|
|||||||
UpdateCronRequest,
|
UpdateCronRequest,
|
||||||
DeleteCronsRequest,
|
DeleteCronsRequest,
|
||||||
CronResponse,
|
CronResponse,
|
||||||
|
GetCronsRequest,
|
||||||
|
CronsResponse,
|
||||||
|
GetCronByIdRequest,
|
||||||
|
EnableCronsRequest,
|
||||||
|
DisableCronsRequest,
|
||||||
|
RunCronsRequest,
|
||||||
} from '../protos/api';
|
} from '../protos/api';
|
||||||
|
import { NotificationInfo } from '../data/notify';
|
||||||
|
|
||||||
Container.set('logger', LoggerInstance);
|
Container.set('logger', LoggerInstance);
|
||||||
|
|
||||||
@@ -39,13 +46,6 @@ export const getEnvs = async (
|
|||||||
callback: sendUnaryData<EnvsResponse>,
|
callback: sendUnaryData<EnvsResponse>,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
if (!call.request.searchValue) {
|
|
||||||
return callback(null, {
|
|
||||||
code: 400,
|
|
||||||
data: [],
|
|
||||||
message: 'searchValue is required',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
const data = await envService.envs(call.request.searchValue);
|
const data = await envService.envs(call.request.searchValue);
|
||||||
callback(null, {
|
callback(null, {
|
||||||
@@ -79,9 +79,17 @@ export const updateEnv = async (
|
|||||||
callback: sendUnaryData<EnvResponse>,
|
callback: sendUnaryData<EnvResponse>,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
if (!call.request.env?.id) {
|
||||||
|
return callback(null, {
|
||||||
|
code: 400,
|
||||||
|
data: undefined,
|
||||||
|
message: 'id parameter is required',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
const data = await envService.update(
|
const data = await envService.update(
|
||||||
pick(call.request.env, ['id', 'name', 'value', 'remark']) as EnvItem,
|
pick(call.request.env, ['id', 'name', 'value', 'remarks']) as EnvItem,
|
||||||
);
|
);
|
||||||
callback(null, { code: 200, data });
|
callback(null, { code: 200, data });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -94,6 +102,13 @@ export const deleteEnvs = async (
|
|||||||
callback: sendUnaryData<Response>,
|
callback: sendUnaryData<Response>,
|
||||||
) => {
|
) => {
|
||||||
try {
|
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);
|
const envService = Container.get(EnvService);
|
||||||
await envService.remove(call.request.ids);
|
await envService.remove(call.request.ids);
|
||||||
callback(null, { code: 200 });
|
callback(null, { code: 200 });
|
||||||
@@ -107,6 +122,14 @@ export const moveEnv = async (
|
|||||||
callback: sendUnaryData<EnvResponse>,
|
callback: sendUnaryData<EnvResponse>,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
if (!call.request.id) {
|
||||||
|
return callback(null, {
|
||||||
|
code: 400,
|
||||||
|
data: undefined,
|
||||||
|
message: 'id parameter is required',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
const data = await envService.move(call.request.id, {
|
const data = await envService.move(call.request.id, {
|
||||||
fromIndex: call.request.fromIndex,
|
fromIndex: call.request.fromIndex,
|
||||||
@@ -123,6 +146,13 @@ export const disableEnvs = async (
|
|||||||
callback: sendUnaryData<Response>,
|
callback: sendUnaryData<Response>,
|
||||||
) => {
|
) => {
|
||||||
try {
|
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);
|
const envService = Container.get(EnvService);
|
||||||
await envService.disabled(call.request.ids);
|
await envService.disabled(call.request.ids);
|
||||||
callback(null, { code: 200 });
|
callback(null, { code: 200 });
|
||||||
@@ -136,6 +166,13 @@ export const enableEnvs = async (
|
|||||||
callback: sendUnaryData<Response>,
|
callback: sendUnaryData<Response>,
|
||||||
) => {
|
) => {
|
||||||
try {
|
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);
|
const envService = Container.get(EnvService);
|
||||||
await envService.enabled(call.request.ids);
|
await envService.enabled(call.request.ids);
|
||||||
callback(null, { code: 200 });
|
callback(null, { code: 200 });
|
||||||
@@ -149,6 +186,13 @@ export const updateEnvNames = async (
|
|||||||
callback: sendUnaryData<Response>,
|
callback: sendUnaryData<Response>,
|
||||||
) => {
|
) => {
|
||||||
try {
|
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);
|
const envService = Container.get(EnvService);
|
||||||
await envService.updateNames({
|
await envService.updateNames({
|
||||||
ids: call.request.ids,
|
ids: call.request.ids,
|
||||||
@@ -165,6 +209,14 @@ export const getEnvById = async (
|
|||||||
callback: sendUnaryData<EnvResponse>,
|
callback: sendUnaryData<EnvResponse>,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
if (!call.request.id) {
|
||||||
|
return callback(null, {
|
||||||
|
code: 400,
|
||||||
|
data: undefined,
|
||||||
|
message: 'id parameter is required',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
const data = await envService.getDb({ id: call.request.id });
|
const data = await envService.getDb({ id: call.request.id });
|
||||||
callback(null, {
|
callback(null, {
|
||||||
@@ -182,7 +234,11 @@ export const systemNotify = async (
|
|||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const systemService = Container.get(SystemService);
|
const systemService = Container.get(SystemService);
|
||||||
const data = await systemService.notify(call.request);
|
const data = await systemService.notify({
|
||||||
|
title: call.request.title,
|
||||||
|
content: call.request.content,
|
||||||
|
notificationInfo: call.request.notificationInfo as unknown as NotificationInfo,
|
||||||
|
});
|
||||||
callback(null, data);
|
callback(null, data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
callback(e);
|
callback(e);
|
||||||
@@ -273,3 +329,116 @@ export const deleteCrons = async (
|
|||||||
callback(e);
|
callback(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCrons = async (
|
||||||
|
call: ServerUnaryCall<GetCronsRequest, CronsResponse>,
|
||||||
|
callback: sendUnaryData<CronsResponse>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const result = await cronService.crontabs({
|
||||||
|
searchValue: call.request.searchValue || '',
|
||||||
|
page: '0',
|
||||||
|
size: '0',
|
||||||
|
sorter: '',
|
||||||
|
filters: '',
|
||||||
|
queryString: '',
|
||||||
|
});
|
||||||
|
const data = result.data.map((x) => normalizeCronData(x as CronItem));
|
||||||
|
callback(null, {
|
||||||
|
code: 200,
|
||||||
|
data: data.filter((x): x is CronItem => x !== undefined),
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(null, {
|
||||||
|
code: 500,
|
||||||
|
data: [],
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCronById = async (
|
||||||
|
call: ServerUnaryCall<GetCronByIdRequest, CronResponse>,
|
||||||
|
callback: sendUnaryData<CronResponse>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
if (!call.request.id) {
|
||||||
|
return callback(null, {
|
||||||
|
code: 400,
|
||||||
|
data: undefined,
|
||||||
|
message: 'id parameter is required',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const data = (await cronService.getDb({ id: call.request.id })) as CronItem;
|
||||||
|
callback(null, { code: 200, data: normalizeCronData(data) });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(null, {
|
||||||
|
code: 404,
|
||||||
|
data: undefined,
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const enableCrons = async (
|
||||||
|
call: ServerUnaryCall<EnableCronsRequest, 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 cronService = Container.get(CronService);
|
||||||
|
await cronService.enabled(call.request.ids);
|
||||||
|
callback(null, { code: 200 });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const disableCrons = async (
|
||||||
|
call: ServerUnaryCall<DisableCronsRequest, 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 cronService = Container.get(CronService);
|
||||||
|
await cronService.disabled(call.request.ids);
|
||||||
|
callback(null, { code: 200 });
|
||||||
|
} catch (e: any) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runCrons = async (
|
||||||
|
call: ServerUnaryCall<RunCronsRequest, 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 cronService = Container.get(CronService);
|
||||||
|
await cronService.run(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());
|
||||||
|
|||||||
@@ -17,14 +17,11 @@ const check = async (
|
|||||||
return callback(null, { status: 1 });
|
return callback(null, { status: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const panelErrLog = await promiseExec(
|
const qinglongErrLog = await promiseExec(
|
||||||
`tail -n 300 ~/.pm2/logs/panel-error.log`,
|
`tail -n 300 ~/.pm2/logs/qinglong-error.log`,
|
||||||
);
|
|
||||||
const scheduleErrLog = await promiseExec(
|
|
||||||
`tail -n 300 ~/.pm2/logs/schedule-error.log`,
|
|
||||||
);
|
);
|
||||||
return callback(
|
return callback(
|
||||||
new Error(`${scheduleErrLog || ''}\n${panelErrLog || ''}\n${res}`.trim()),
|
new Error(`${qinglongErrLog || ''}\n${res}`.trim()),
|
||||||
);
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,27 +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';
|
|
||||||
import { ApiService } from '../protos/api';
|
|
||||||
import * as Api from './api';
|
|
||||||
|
|
||||||
const server = new Server({ 'grpc.enable_http_proxy': 0 });
|
|
||||||
server.addService(HealthService, { check });
|
|
||||||
server.addService(CronService, { addCron, delCron });
|
|
||||||
server.addService(ApiService, Api);
|
|
||||||
server.bindAsync(
|
|
||||||
`0.0.0.0:${config.cronPort}`,
|
|
||||||
ServerCredentials.createInsecure(),
|
|
||||||
(err, port) => {
|
|
||||||
if (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
Logger.debug(`✌️ 定时服务启动成功!`);
|
|
||||||
console.debug(`✌️ 定时服务启动成功!`);
|
|
||||||
process.send?.('ready');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -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 {
|
||||||
@@ -27,10 +27,10 @@ export default class ConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
+111
-40
@@ -4,13 +4,15 @@ import config from '../config';
|
|||||||
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||||
import { exec, execSync } from 'child_process';
|
import { exec, execSync } from 'child_process';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import cron_parser from 'cron-parser';
|
import { CronExpressionParser } from 'cron-parser';
|
||||||
import {
|
import {
|
||||||
getFileContentByName,
|
getFileContentByName,
|
||||||
fileExist,
|
fileExist,
|
||||||
killTask,
|
killTask,
|
||||||
|
killAllTasks,
|
||||||
getUniqPath,
|
getUniqPath,
|
||||||
safeJSONParse,
|
safeJSONParse,
|
||||||
|
isDemoEnv,
|
||||||
} from '../config/util';
|
} from '../config/util';
|
||||||
import { Op, where, col as colFn, FindOptions, fn, Order } from 'sequelize';
|
import { Op, where, col as colFn, FindOptions, fn, Order } from 'sequelize';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -23,6 +25,7 @@ import pickBy from 'lodash/pickBy';
|
|||||||
import omit from 'lodash/omit';
|
import omit from 'lodash/omit';
|
||||||
import { writeFileWithLock } from '../shared/utils';
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
import { ScheduleType } from '../interface/schedule';
|
import { ScheduleType } from '../interface/schedule';
|
||||||
|
import { logStreamManager } from '../shared/logStreamManager';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CronService {
|
export default class CronService {
|
||||||
@@ -48,11 +51,35 @@ export default class CronService {
|
|||||||
return this.isOnceSchedule(schedule) || this.isBootSchedule(schedule);
|
return this.isOnceSchedule(schedule) || this.isBootSchedule(schedule);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getLogName(cron: Crontab) {
|
||||||
|
const { log_name, command, id } = cron;
|
||||||
|
if (log_name === '/dev/null') {
|
||||||
|
return log_name;
|
||||||
|
}
|
||||||
|
let uniqPath = await getUniqPath(command, `${id}`);
|
||||||
|
if (log_name) {
|
||||||
|
const normalizedLogName = log_name.startsWith('/')
|
||||||
|
? log_name
|
||||||
|
: path.join(config.logPath, log_name);
|
||||||
|
if (normalizedLogName.startsWith(config.logPath)) {
|
||||||
|
uniqPath = log_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
|
||||||
|
await fs.mkdir(logDirPath, { recursive: true });
|
||||||
|
return uniqPath;
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
tab.log_name = await this.getLogName(tab);
|
||||||
const doc = await this.insert(tab);
|
const doc = await this.insert(tab);
|
||||||
|
|
||||||
|
if (isDemoEnv()) {
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isNodeCron(doc) && !this.isSpecialSchedule(doc.schedule)) {
|
if (this.isNodeCron(doc) && !this.isSpecialSchedule(doc.schedule)) {
|
||||||
await cronClient.addCron([
|
await cronClient.addCron([
|
||||||
{
|
{
|
||||||
@@ -77,9 +104,10 @@ export default class CronService {
|
|||||||
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;
|
||||||
|
tab.log_name = await this.getLogName(tab);
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,14 +238,24 @@ export default class CronService {
|
|||||||
operate2 = Op.and;
|
operate2 = Op.and;
|
||||||
break;
|
break;
|
||||||
case 'In':
|
case 'In':
|
||||||
q[Op.or] = [
|
if (
|
||||||
{
|
property === 'status' &&
|
||||||
[property]: Array.isArray(value) ? value : [value],
|
!value.includes(CrontabStatus.disabled)
|
||||||
},
|
) {
|
||||||
property === 'status' && value.includes(2)
|
q[Op.and] = [
|
||||||
? { isDisabled: 1 }
|
{ [property]: Array.isArray(value) ? value : [value] },
|
||||||
: {},
|
{ isDisabled: 0 },
|
||||||
];
|
];
|
||||||
|
} else {
|
||||||
|
q[Op.or] = [
|
||||||
|
{
|
||||||
|
[property]: Array.isArray(value) ? value : [value],
|
||||||
|
},
|
||||||
|
property === 'status' && value.includes(CrontabStatus.disabled)
|
||||||
|
? { isDisabled: 1 }
|
||||||
|
: {},
|
||||||
|
];
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'Nin':
|
case 'Nin':
|
||||||
q[Op.and] = [
|
q[Op.and] = [
|
||||||
@@ -427,12 +465,17 @@ export default class CronService {
|
|||||||
public async stop(ids: number[]) {
|
public async stop(ids: number[]) {
|
||||||
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
if (doc.pid) {
|
// Kill all running instances of this task
|
||||||
try {
|
try {
|
||||||
await killTask(doc.pid);
|
const command = this.makeCommand(doc);
|
||||||
} catch (error) {
|
await killAllTasks(command);
|
||||||
this.logger.error(error);
|
this.logger.info(
|
||||||
}
|
`[panel][停止所有运行中的任务实例] 任务ID: ${doc.id}, 命令: ${command}`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`[panel][停止任务失败] 任务ID: ${doc.id}, 错误: ${error}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,13 +504,15 @@ export default class CronService {
|
|||||||
`[panel][开始执行任务] 参数: ${JSON.stringify(params)}`,
|
`[panel][开始执行任务] 参数: ${JSON.stringify(params)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
let { id, command, log_path } = cron;
|
let { id, command, log_name } = cron;
|
||||||
const uniqPath = await getUniqPath(command, `${id}`);
|
|
||||||
|
const uniqPath =
|
||||||
|
log_name === '/dev/null' || !log_name
|
||||||
|
? await getUniqPath(command, `${id}`)
|
||||||
|
: log_name;
|
||||||
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
||||||
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
|
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
|
||||||
if (log_path?.split('/')?.every((x) => x !== uniqPath)) {
|
await fs.mkdir(logDirPath, { recursive: true });
|
||||||
await fs.mkdir(logDirPath, { recursive: true });
|
|
||||||
}
|
|
||||||
const logPath = `${uniqPath}/${logTime}.log`;
|
const logPath = `${uniqPath}/${logTime}.log`;
|
||||||
const absolutePath = path.resolve(config.logPath, `${logPath}`);
|
const absolutePath = path.resolve(config.logPath, `${logPath}`);
|
||||||
const cp = spawn(
|
const cp = spawn(
|
||||||
@@ -483,7 +528,7 @@ export default class CronService {
|
|||||||
{ where: { id } },
|
{ where: { id } },
|
||||||
);
|
);
|
||||||
cp.stdout.on('data', async (data) => {
|
cp.stdout.on('data', async (data) => {
|
||||||
await fs.appendFile(absolutePath, data.toString());
|
await logStreamManager.write(absolutePath, data.toString());
|
||||||
});
|
});
|
||||||
cp.stderr.on('data', async (data) => {
|
cp.stderr.on('data', async (data) => {
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
@@ -491,7 +536,7 @@ export default class CronService {
|
|||||||
command,
|
command,
|
||||||
data.toString(),
|
data.toString(),
|
||||||
);
|
);
|
||||||
await fs.appendFile(absolutePath, data.toString());
|
await logStreamManager.write(absolutePath, data.toString());
|
||||||
});
|
});
|
||||||
cp.on('error', async (err) => {
|
cp.on('error', async (err) => {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
@@ -499,7 +544,7 @@ export default class CronService {
|
|||||||
command,
|
command,
|
||||||
err,
|
err,
|
||||||
);
|
);
|
||||||
await fs.appendFile(absolutePath, JSON.stringify(err));
|
await logStreamManager.write(absolutePath, JSON.stringify(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
cp.on('exit', async (code) => {
|
cp.on('exit', async (code) => {
|
||||||
@@ -508,6 +553,8 @@ export default class CronService {
|
|||||||
JSON.stringify(params),
|
JSON.stringify(params),
|
||||||
code,
|
code,
|
||||||
);
|
);
|
||||||
|
// Close the stream after task completion
|
||||||
|
await logStreamManager.closeStream(absolutePath);
|
||||||
await CrontabModel.update(
|
await CrontabModel.update(
|
||||||
{ status: CrontabStatus.idle, pid: undefined },
|
{ status: CrontabStatus.idle, pid: undefined },
|
||||||
{ where: { id } },
|
{ where: { id } },
|
||||||
@@ -528,7 +575,7 @@ export default class CronService {
|
|||||||
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),
|
||||||
@@ -536,6 +583,10 @@ export default class CronService {
|
|||||||
command: this.makeCommand(doc),
|
command: this.makeCommand(doc),
|
||||||
extra_schedules: doc.extra_schedules || [],
|
extra_schedules: doc.extra_schedules || [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (isDemoEnv()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await cronClient.addCron(sixCron);
|
await cronClient.addCron(sixCron);
|
||||||
await this.setCrontab();
|
await this.setCrontab();
|
||||||
}
|
}
|
||||||
@@ -545,13 +596,18 @@ export default class CronService {
|
|||||||
if (!doc) {
|
if (!doc) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
if (doc.log_name === '/dev/null') {
|
||||||
|
return '日志设置为忽略';
|
||||||
|
}
|
||||||
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
||||||
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
||||||
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)
|
||||||
|
? '运行中...'
|
||||||
|
: '日志不存在...';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,7 +627,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);
|
||||||
@@ -585,9 +641,11 @@ export default class CronService {
|
|||||||
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
|
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
|
||||||
command = `${TASK_PREFIX}${tab.command}`;
|
command = `${TASK_PREFIX}${tab.command}`;
|
||||||
}
|
}
|
||||||
let commandVariable = `real_time=${Boolean(realTime)} no_tee=true ID=${
|
let commandVariable = `real_time=${Boolean(realTime)} no_tee=true ID=${tab.id} `;
|
||||||
tab.id
|
// Only include log_name if it has a truthy value to avoid passing null/undefined to shell
|
||||||
} `;
|
if (tab.log_name) {
|
||||||
|
commandVariable += `log_name=${tab.log_name} `;
|
||||||
|
}
|
||||||
if (tab.task_before) {
|
if (tab.task_before) {
|
||||||
commandVariable += `task_before='${tab.task_before
|
commandVariable += `task_before='${tab.task_before
|
||||||
.replace(/'/g, "'\\''")
|
.replace(/'/g, "'\\''")
|
||||||
@@ -609,11 +667,9 @@ export default class CronService {
|
|||||||
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)
|
this.isSpecialSchedule(tab.schedule)
|
||||||
) {
|
) {
|
||||||
crontab_string += '# ';
|
crontab_string += '# ';
|
||||||
@@ -631,12 +687,23 @@ export default class CronService {
|
|||||||
|
|
||||||
await writeFileWithLock(config.crontabFile, crontab_string);
|
await writeFileWithLock(config.crontabFile, crontab_string);
|
||||||
|
|
||||||
execSync(`crontab ${config.crontabFile}`);
|
try {
|
||||||
|
execSync(`crontab ${config.crontabFile}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMsg = error.message || String(error);
|
||||||
|
this.logger.error('[crontab] Failed to update system crontab:', errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
await CrontabModel.update({ saved: true }, { where: {} });
|
await CrontabModel.update({ saved: true }, { where: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
public importCrontab() {
|
public importCrontab() {
|
||||||
exec('crontab -l', (error, stdout, stderr) => {
|
exec('crontab -l', (error, stdout) => {
|
||||||
|
if (error) {
|
||||||
|
const errorMsg = error.message || String(error);
|
||||||
|
this.logger.error('[crontab] Failed to read system crontab:', errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
const lines = stdout.split('\n');
|
const lines = stdout.split('\n');
|
||||||
const namePrefix = new Date().getTime();
|
const namePrefix = new Date().getTime();
|
||||||
|
|
||||||
@@ -650,7 +717,7 @@ export default class CronService {
|
|||||||
if (
|
if (
|
||||||
command &&
|
command &&
|
||||||
schedule &&
|
schedule &&
|
||||||
cron_parser.parseExpression(schedule).hasNext()
|
CronExpressionParser.parse(schedule).hasNext()
|
||||||
) {
|
) {
|
||||||
const name = namePrefix + '_' + index;
|
const name = namePrefix + '_' + index;
|
||||||
|
|
||||||
@@ -671,13 +738,11 @@ export default class CronService {
|
|||||||
|
|
||||||
public async autosave_crontab() {
|
public async autosave_crontab() {
|
||||||
const tabs = await this.crontabs();
|
const tabs = await this.crontabs();
|
||||||
this.setCrontab(tabs);
|
|
||||||
|
|
||||||
const regularCrons = tabs.data
|
const regularCrons = tabs.data
|
||||||
.filter(
|
.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
this.isNodeCron(x) &&
|
|
||||||
x.isDisabled !== 1 &&
|
x.isDisabled !== 1 &&
|
||||||
|
this.isNodeCron(x) &&
|
||||||
!this.isSpecialSchedule(x.schedule),
|
!this.isSpecialSchedule(x.schedule),
|
||||||
)
|
)
|
||||||
.map((doc) => ({
|
.map((doc) => ({
|
||||||
@@ -687,7 +752,13 @@ export default class CronService {
|
|||||||
command: this.makeCommand(doc),
|
command: this.makeCommand(doc),
|
||||||
extra_schedules: doc.extra_schedules || [],
|
extra_schedules: doc.extra_schedules || [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (isDemoEnv()) {
|
||||||
|
await writeFileWithLock(config.crontabFile, '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
await cronClient.addCron(regularCrons);
|
await cronClient.addCron(regularCrons);
|
||||||
|
this.setCrontab(tabs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async bootTask() {
|
public async bootTask() {
|
||||||
|
|||||||
+36
-43
@@ -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';
|
||||||
@@ -28,7 +28,7 @@ export default class DependenceService {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject('logger') private logger: winston.Logger,
|
@Inject('logger') private logger: winston.Logger,
|
||||||
private sockService: SockService,
|
private sockService: SockService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
public async create(payloads: Dependence[]): Promise<Dependence[]> {
|
public async create(payloads: Dependence[]): Promise<Dependence[]> {
|
||||||
const tabs = payloads.map((x) => {
|
const tabs = payloads.map((x) => {
|
||||||
@@ -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({
|
||||||
|
|||||||
+12
-4
@@ -1,7 +1,8 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import groupBy from 'lodash/groupBy';
|
||||||
|
import { FindOptions, Op } from 'sequelize';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs/promises';
|
|
||||||
import {
|
import {
|
||||||
Env,
|
Env,
|
||||||
EnvModel,
|
EnvModel,
|
||||||
@@ -11,8 +12,6 @@ import {
|
|||||||
minPosition,
|
minPosition,
|
||||||
stepPosition,
|
stepPosition,
|
||||||
} from '../data/env';
|
} from '../data/env';
|
||||||
import groupBy from 'lodash/groupBy';
|
|
||||||
import { FindOptions, Op } from 'sequelize';
|
|
||||||
import { writeFileWithLock } from '../shared/utils';
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -147,6 +146,7 @@ export default class EnvService {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await this.find(condition, [
|
const result = await this.find(condition, [
|
||||||
|
['isPinned', 'DESC'],
|
||||||
['position', 'DESC'],
|
['position', 'DESC'],
|
||||||
['createdAt', 'ASC'],
|
['createdAt', 'ASC'],
|
||||||
]);
|
]);
|
||||||
@@ -190,6 +190,14 @@ export default class EnvService {
|
|||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async pin(ids: number[]) {
|
||||||
|
await EnvModel.update({ isPinned: 1 }, { where: { id: ids } });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async unPin(ids: number[]) {
|
||||||
|
await EnvModel.update({ isPinned: 0 }, { where: { id: ids } });
|
||||||
|
}
|
||||||
|
|
||||||
public async set_envs() {
|
public async set_envs() {
|
||||||
const envs = await this.envs('', {
|
const envs = await this.envs('', {
|
||||||
name: { [Op.not]: null },
|
name: { [Op.not]: null },
|
||||||
|
|||||||
@@ -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
-186
@@ -1,11 +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 { 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 {
|
||||||
@@ -49,17 +49,26 @@ export default class NotificationService {
|
|||||||
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;
|
||||||
@@ -84,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,
|
||||||
@@ -95,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 {
|
||||||
@@ -110,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 {
|
||||||
@@ -136,15 +144,13 @@ export default class NotificationService {
|
|||||||
: `https://sctapi.ftqq.com/${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=${encodeURIComponent(
|
||||||
body: `title=${encodeURIComponent(
|
this.title,
|
||||||
this.title,
|
)}&desp=${encodeURIComponent(this.content)}`,
|
||||||
)}&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 {
|
||||||
@@ -159,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
|
||||||
@@ -184,13 +188,11 @@ export default class NotificationService {
|
|||||||
private async chat() {
|
private async chat() {
|
||||||
const { synologyChatUrl } = this.params;
|
const { synologyChatUrl } = this.params;
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
const res = await httpClient.post(synologyChatUrl, {
|
||||||
.post(synologyChatUrl, {
|
...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 {
|
||||||
@@ -226,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 {
|
||||||
@@ -258,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' },
|
dispatcher: agent,
|
||||||
agent,
|
});
|
||||||
})
|
|
||||||
.json();
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -303,17 +291,15 @@ 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',
|
text: {
|
||||||
text: {
|
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 {
|
||||||
@@ -329,17 +315,15 @@ 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',
|
text: {
|
||||||
text: {
|
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 {
|
||||||
@@ -356,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',
|
||||||
@@ -405,20 +387,18 @@ 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,
|
json: {
|
||||||
json: {
|
touser,
|
||||||
touser,
|
agentid,
|
||||||
agentid,
|
safe: '0',
|
||||||
safe: '0',
|
...options,
|
||||||
...options,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)
|
},
|
||||||
.json();
|
);
|
||||||
|
|
||||||
if (res.errcode === 0) {
|
if (res.errcode === 0) {
|
||||||
return true;
|
return true;
|
||||||
@@ -460,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 {
|
||||||
@@ -482,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;
|
||||||
@@ -527,7 +503,7 @@ export default class NotificationService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const res: any = await got.post(url, body).json();
|
const res = await httpClient.post(url, body);
|
||||||
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
return true;
|
return true;
|
||||||
@@ -551,19 +527,17 @@ 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}`,
|
title: `${this.title}`,
|
||||||
title: `${this.title}`,
|
template: `${template}`,
|
||||||
template: `${template}`,
|
content: `${content}`,
|
||||||
content: `${content}`,
|
receiver: `${wePlusBotReceiver || ''}`,
|
||||||
receiver: `${wePlusBotReceiver || ''}`,
|
version: `${wePlusBotVersion || 'pro'}`,
|
||||||
version: `${wePlusBotVersion || 'pro'}`,
|
},
|
||||||
},
|
});
|
||||||
})
|
|
||||||
.json();
|
|
||||||
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
return true;
|
return true;
|
||||||
@@ -576,23 +550,35 @@ export default class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async lark() {
|
private async lark() {
|
||||||
let { larkKey } = this.params;
|
let { larkKey, larkSecret } = this.params;
|
||||||
|
|
||||||
if (!larkKey.startsWith('http')) {
|
if (!larkKey.startsWith('http')) {
|
||||||
larkKey = `https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`;
|
larkKey = `https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const body: Record<string, any> = {
|
||||||
|
msg_type: 'text',
|
||||||
|
content: { text: `${this.title}\n\n${this.content}` },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add signature if secret is provided
|
||||||
|
// Note: Feishu's signature algorithm uses timestamp+"\n"+secret as the HMAC key
|
||||||
|
// and signs an empty message, which differs from typical HMAC usage
|
||||||
|
if (larkSecret) {
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||||
|
const stringToSign = `${timestamp}\n${larkSecret}`;
|
||||||
|
const hmac = crypto.createHmac('sha256', stringToSign);
|
||||||
|
const sign = hmac.digest('base64');
|
||||||
|
body.timestamp = timestamp;
|
||||||
|
body.sign = sign;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
const res = await httpClient.post(larkKey, {
|
||||||
.post(larkKey, {
|
...this.gotOption,
|
||||||
...this.gotOption,
|
json: body,
|
||||||
json: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
msg_type: 'text',
|
});
|
||||||
content: { text: `${this.title}\n\n${this.content}` },
|
|
||||||
},
|
|
||||||
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 {
|
||||||
@@ -604,7 +590,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({
|
||||||
@@ -617,7 +603,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/>')}`,
|
||||||
});
|
});
|
||||||
@@ -637,19 +623,22 @@ 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'>(
|
||||||
...this.gotOption,
|
pushMeUrl || 'https://push.i-i.me/',
|
||||||
json: {
|
{
|
||||||
push_key: pushMeKey,
|
...this.gotOption,
|
||||||
title: this.title,
|
json: {
|
||||||
content: this.content,
|
push_key: pushMeKey,
|
||||||
|
title: this.title,
|
||||||
|
content: this.content,
|
||||||
|
},
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
},
|
},
|
||||||
headers: { 'Content-Type': 'application/json' },
|
);
|
||||||
});
|
if (res === 'success') {
|
||||||
if (res.body === 'success') {
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.body);
|
throw new Error(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);
|
||||||
@@ -657,26 +646,49 @@ export default class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async ntfy() {
|
private async ntfy() {
|
||||||
const { ntfyUrl, ntfyTopic, ntfyPriority } = this.params;
|
const {
|
||||||
|
ntfyUrl,
|
||||||
|
ntfyTopic,
|
||||||
|
ntfyPriority,
|
||||||
|
ntfyToken,
|
||||||
|
ntfyUsername,
|
||||||
|
ntfyPassword,
|
||||||
|
ntfyActions,
|
||||||
|
} = this.params;
|
||||||
// 编码函数
|
// 编码函数
|
||||||
const encodeRfc2047 = (text: string, charset: string = 'UTF-8'): string => {
|
const encodeRfc2047 = (text: string, charset: string = 'UTF-8'): string => {
|
||||||
const encodedText = Buffer.from(text).toString('base64');
|
const encodedText = Buffer.from(text).toString('base64');
|
||||||
return `=?${charset}?B?${encodedText}?=`;
|
return `=?${charset}?B?${encodedText}?=`;
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const encodedTitle = encodeRfc2047(this.title);
|
const headers: Record<string, string> = {
|
||||||
const res: any = await got.post(
|
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}`,
|
`${ntfyUrl || 'https://ntfy.sh'}/${ntfyTopic}`,
|
||||||
{
|
{
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
body: `${this.content}`,
|
body: `${this.content}`,
|
||||||
headers: { Title: encodedTitle, Priority: `${ntfyPriority || '3'}` },
|
headers: headers,
|
||||||
|
method: 'POST',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (res.statusCode === 200) {
|
if (res.statusCode === 200) {
|
||||||
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);
|
||||||
@@ -710,20 +722,18 @@ export default class NotificationService {
|
|||||||
|
|
||||||
const url = `https://wxpusher.zjiecode.com/api/send/message`;
|
const url = `https://wxpusher.zjiecode.com/api/send/message`;
|
||||||
try {
|
try {
|
||||||
const res: any = await got
|
const res = await httpClient.post(url, {
|
||||||
.post(url, {
|
...this.gotOption,
|
||||||
...this.gotOption,
|
json: {
|
||||||
json: {
|
appToken: wxPusherBotAppToken,
|
||||||
appToken: wxPusherBotAppToken,
|
content: `<h1>${this.title}</h1><br/><div style='white-space: pre-wrap;'>${this.content}</div>`,
|
||||||
content: `<h1>${this.title}</h1><br/><div style='white-space: pre-wrap;'>${this.content}</div>`,
|
summary: this.title,
|
||||||
summary: this.title,
|
contentType: 2,
|
||||||
contentType: 2,
|
topicIds: topicIds,
|
||||||
topicIds: topicIds,
|
uids: uids,
|
||||||
uids: uids,
|
verifyPayType: 0,
|
||||||
verifyPayType: 0,
|
},
|
||||||
},
|
});
|
||||||
})
|
|
||||||
.json();
|
|
||||||
|
|
||||||
if (res.code === 1000) {
|
if (res.code === 1000) {
|
||||||
return true;
|
return true;
|
||||||
@@ -774,15 +784,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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -817,15 +828,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);
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export default class ScheduleService {
|
|||||||
) {
|
) {
|
||||||
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,
|
||||||
@@ -172,7 +172,7 @@ export default class ScheduleService {
|
|||||||
|
|
||||||
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);
|
||||||
@@ -187,7 +187,7 @@ export default class ScheduleService {
|
|||||||
) {
|
) {
|
||||||
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,
|
||||||
@@ -232,7 +232,7 @@ export default class ScheduleService {
|
|||||||
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(
|
this.logger.info(
|
||||||
'[panel][取消interval任务], 任务ID: %s, 任务名: %s',
|
'[panel][取消interval任务] 任务ID: %s, 任务名: %s',
|
||||||
_id,
|
_id,
|
||||||
name,
|
name,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -64,10 +64,15 @@ export default class ScriptService {
|
|||||||
return { code: 200 };
|
return { code: 200 };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFile(filePath: string, fileName: string) {
|
public checkFilePath(filePath: string, fileName: string) {
|
||||||
const finalPath = path.resolve(config.scriptPath, filePath, fileName);
|
const finalPath = path.resolve(config.scriptPath, filePath, fileName);
|
||||||
|
return finalPath.startsWith(config.scriptPath) ? finalPath : '';
|
||||||
|
}
|
||||||
|
|
||||||
if (!finalPath.startsWith(config.scriptPath)) {
|
public async getFile(filePath: string, fileName: string) {
|
||||||
|
const finalPath = this.checkFilePath(filePath, fileName);
|
||||||
|
|
||||||
|
if (!finalPath) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+34
-2
@@ -26,12 +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 writeFileWithLock(this.sshConfigFilePath, '');
|
await writeFileWithLock(this.sshConfigFilePath, '', { mode: '600' });
|
||||||
}
|
}
|
||||||
if (!config.includes(this.sshConfigHeader)) {
|
if (!config.includes(this.sshConfigHeader)) {
|
||||||
await writeFileWithLock(
|
await writeFileWithLock(
|
||||||
this.sshConfigFilePath,
|
this.sshConfigFilePath,
|
||||||
`${this.sshConfigHeader}\n\n${config}`,
|
`${this.sshConfigHeader}\n\n${config}`,
|
||||||
|
{ mode: '600' },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,7 +46,6 @@ export default class SshKeyService {
|
|||||||
path.join(this.sshPath, alias),
|
path.join(this.sshPath, alias),
|
||||||
`${key}${os.EOL}`,
|
`${key}${os.EOL}`,
|
||||||
{
|
{
|
||||||
encoding: 'utf8',
|
|
||||||
mode: '400',
|
mode: '400',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -81,6 +81,10 @@ export default class SshKeyService {
|
|||||||
await writeFileWithLock(
|
await writeFileWithLock(
|
||||||
`${path.join(this.sshPath, `${alias}.config`)}`,
|
`${path.join(this.sshPath, `${alias}.config`)}`,
|
||||||
config,
|
config,
|
||||||
|
{
|
||||||
|
encoding: 'utf8',
|
||||||
|
mode: '600',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,4 +131,32 @@ export default class SshKeyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async addGlobalSSHKey(key: string, alias: string): Promise<void> {
|
||||||
|
await this.generatePrivateKeyFile(`~global_${alias}`, key);
|
||||||
|
// Create a global SSH config entry that matches all hosts
|
||||||
|
// This allows the key to be used for any Git repository
|
||||||
|
await this.generateGlobalSshConfig(`~global_${alias}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeGlobalSSHKey(alias: string): Promise<void> {
|
||||||
|
await this.removePrivateKeyFile(`~global_${alias}`);
|
||||||
|
await this.removeSshConfig(`~global_${alias}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateGlobalSshConfig(alias: string) {
|
||||||
|
// Create a config that matches all hosts, making this key globally available
|
||||||
|
const config = `Host *\n IdentityFile ${path.join(
|
||||||
|
this.sshPath,
|
||||||
|
alias,
|
||||||
|
)}\n StrictHostKeyChecking no\n`;
|
||||||
|
await writeFileWithLock(
|
||||||
|
`${path.join(this.sshPath, `${alias}.config`)}`,
|
||||||
|
config,
|
||||||
|
{
|
||||||
|
encoding: 'utf8',
|
||||||
|
mode: '600',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ 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';
|
import taskLimit from '../shared/pLimit';
|
||||||
|
import { logStreamManager } from '../shared/logStreamManager';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SubscriptionService {
|
export default class SubscriptionService {
|
||||||
@@ -136,7 +137,7 @@ export default class SubscriptionService {
|
|||||||
let beforeStr = '';
|
let beforeStr = '';
|
||||||
try {
|
try {
|
||||||
if (doc.sub_before) {
|
if (doc.sub_before) {
|
||||||
await fs.appendFile(absolutePath, `\n## 执行before命令...\n\n`);
|
await logStreamManager.write(absolutePath, `\n## 执行before命令...\n\n`);
|
||||||
beforeStr = await promiseExec(doc.sub_before);
|
beforeStr = await promiseExec(doc.sub_before);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -144,7 +145,7 @@ export default class SubscriptionService {
|
|||||||
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
|
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
|
||||||
}
|
}
|
||||||
if (beforeStr) {
|
if (beforeStr) {
|
||||||
await fs.appendFile(absolutePath, `${beforeStr}\n`);
|
await logStreamManager.write(absolutePath, `${beforeStr}\n`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStart: async (cp: ChildProcessWithoutNullStreams, startTime) => {
|
onStart: async (cp: ChildProcessWithoutNullStreams, startTime) => {
|
||||||
@@ -163,7 +164,7 @@ export default class SubscriptionService {
|
|||||||
let afterStr = '';
|
let afterStr = '';
|
||||||
try {
|
try {
|
||||||
if (sub.sub_after) {
|
if (sub.sub_after) {
|
||||||
await fs.appendFile(absolutePath, `\n\n## 执行after命令...\n\n`);
|
await logStreamManager.write(absolutePath, `\n\n## 执行after命令...\n\n`);
|
||||||
afterStr = await promiseExec(sub.sub_after);
|
afterStr = await promiseExec(sub.sub_after);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -171,16 +172,19 @@ export default class SubscriptionService {
|
|||||||
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
|
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
|
||||||
}
|
}
|
||||||
if (afterStr) {
|
if (afterStr) {
|
||||||
await fs.appendFile(absolutePath, `${afterStr}\n`);
|
await logStreamManager.write(absolutePath, `${afterStr}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.appendFile(
|
await logStreamManager.write(
|
||||||
absolutePath,
|
absolutePath,
|
||||||
`\n## 执行结束... ${endTime.format(
|
`\n## 执行结束... ${endTime.format(
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
)} 耗时 ${diff} 秒${LOG_END_SYMBOL}`,
|
)} 耗时 ${diff} 秒${LOG_END_SYMBOL}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Close the stream after task completion
|
||||||
|
await logStreamManager.closeStream(absolutePath);
|
||||||
|
|
||||||
await SubscriptionModel.update(
|
await SubscriptionModel.update(
|
||||||
{ status: SubscriptionStatus.idle, pid: undefined },
|
{ status: SubscriptionStatus.idle, pid: undefined },
|
||||||
{ where: { id: sub.id } },
|
{ where: { id: sub.id } },
|
||||||
@@ -195,12 +199,12 @@ export default class SubscriptionService {
|
|||||||
onError: async (message: string) => {
|
onError: async (message: string) => {
|
||||||
const sub = await this.getDb({ id: doc.id });
|
const sub = await this.getDb({ id: doc.id });
|
||||||
const absolutePath = await handleLogPath(sub.log_path as string);
|
const absolutePath = await handleLogPath(sub.log_path as string);
|
||||||
await fs.appendFile(absolutePath, `\n${message}`);
|
await logStreamManager.write(absolutePath, `\n${message}`);
|
||||||
},
|
},
|
||||||
onLog: async (message: string) => {
|
onLog: async (message: string) => {
|
||||||
const sub = await this.getDb({ id: doc.id });
|
const sub = await this.getDb({ id: doc.id });
|
||||||
const absolutePath = await handleLogPath(sub.log_path as string);
|
const absolutePath = await handleLogPath(sub.log_path as string);
|
||||||
await fs.appendFile(absolutePath, `\n${message}`);
|
await logStreamManager.write(absolutePath, `\n${message}`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -384,7 +388,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);
|
||||||
|
|||||||
+83
-14
@@ -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,
|
||||||
@@ -47,7 +47,7 @@ export default class SystemService {
|
|||||||
@Inject('logger') private logger: winston.Logger,
|
@Inject('logger') private logger: winston.Logger,
|
||||||
private scheduleService: ScheduleService,
|
private scheduleService: ScheduleService,
|
||||||
private sockService: SockService,
|
private sockService: SockService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
public async getSystemConfig() {
|
public async getSystemConfig() {
|
||||||
const doc = await this.getDb({ type: AuthDataType.systemConfig });
|
const doc = await this.getDb({ type: AuthDataType.systemConfig });
|
||||||
@@ -276,14 +276,18 @@ 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 = parseContentVersion(result.body);
|
const text = await body.text();
|
||||||
} catch (error) {}
|
lastVersionContent = parseContentVersion(text);
|
||||||
|
} catch (error) { }
|
||||||
|
|
||||||
if (!lastVersionContent) {
|
if (!lastVersionContent) {
|
||||||
lastVersionContent = currentVersionContent;
|
lastVersionContent = currentVersionContent;
|
||||||
@@ -357,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 {
|
||||||
@@ -371,11 +401,12 @@ 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, '-'),
|
id: command.replace(/ /g, '-'),
|
||||||
runOrigin: 'system',
|
runOrigin: 'system',
|
||||||
@@ -404,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.dataPath} && cd ../ && 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) {
|
||||||
@@ -492,4 +529,36 @@ export default class SystemService {
|
|||||||
return { code: 400, message: '设置时区失败' };
|
return { code: 400, message: '设置时区失败' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updateGlobalSshKey(info: SystemModelInfo) {
|
||||||
|
const oDoc = await this.getSystemConfig();
|
||||||
|
const result = await this.updateAuthDb({
|
||||||
|
...oDoc,
|
||||||
|
info: { ...oDoc.info, ...info },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply the global SSH key
|
||||||
|
const SshKeyService = require('./sshKey').default;
|
||||||
|
const Container = require('typedi').Container;
|
||||||
|
const sshKeyService = Container.get(SshKeyService);
|
||||||
|
|
||||||
|
if (info.globalSshKey) {
|
||||||
|
await sshKeyService.addGlobalSSHKey(info.globalSshKey, 'global');
|
||||||
|
} else {
|
||||||
|
await sshKeyService.removeGlobalSSHKey('global');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { code: 200, data: result };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+175
-19
@@ -1,6 +1,6 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { createRandomString, getNetIp } from '../config/util';
|
import { createRandomString } from '../config/util';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { authenticator } from '@otplib/preset-default';
|
import { authenticator } from '@otplib/preset-default';
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
SystemModelInfo,
|
SystemModelInfo,
|
||||||
LoginStatus,
|
LoginStatus,
|
||||||
AuthInfo,
|
AuthInfo,
|
||||||
|
TokenInfo,
|
||||||
} from '../data/system';
|
} from '../data/system';
|
||||||
import { NotificationInfo } from '../data/notify';
|
import { NotificationInfo } from '../data/notify';
|
||||||
import NotificationService from './notify';
|
import NotificationService from './notify';
|
||||||
@@ -21,6 +22,8 @@ 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';
|
import { shareStore } from '../shared/store';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -93,18 +96,29 @@ export default class UserService {
|
|||||||
}
|
}
|
||||||
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',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tokenInfo: TokenInfo = {
|
||||||
|
value: token,
|
||||||
|
timestamp,
|
||||||
|
ip,
|
||||||
|
address,
|
||||||
|
platform: req.platform,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedTokens = this.addTokenToList(
|
||||||
|
tokens,
|
||||||
|
req.platform,
|
||||||
|
tokenInfo,
|
||||||
|
);
|
||||||
|
|
||||||
await this.updateAuthInfo(content, {
|
await this.updateAuthInfo(content, {
|
||||||
token,
|
token,
|
||||||
tokens: {
|
tokens: updatedTokens,
|
||||||
...tokens,
|
|
||||||
[req.platform]: token,
|
|
||||||
},
|
|
||||||
lastlogon: timestamp,
|
lastlogon: timestamp,
|
||||||
retries: 0,
|
retries: 0,
|
||||||
lastip: ip,
|
lastip: ip,
|
||||||
@@ -131,7 +145,14 @@ export default class UserService {
|
|||||||
this.getLoginLog();
|
this.getLoginLog();
|
||||||
return {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
data: { token, lastip, lastaddr, lastlogon, retries, platform },
|
data: {
|
||||||
|
token,
|
||||||
|
lastip,
|
||||||
|
lastaddr,
|
||||||
|
lastlogon,
|
||||||
|
retries,
|
||||||
|
platform,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
await this.updateAuthInfo(content, {
|
await this.updateAuthInfo(content, {
|
||||||
@@ -171,11 +192,37 @@ export default class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async logout(platform: string): Promise<any> {
|
public async logout(platform: string, tokenValue: string): Promise<any> {
|
||||||
|
if (!platform || !tokenValue) {
|
||||||
|
this.logger.warn('Invalid logout parameters - empty platform or token');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const authInfo = await this.getAuthInfo();
|
const authInfo = await this.getAuthInfo();
|
||||||
|
|
||||||
|
// Verify the token exists before attempting to remove it
|
||||||
|
const tokenExists = this.findTokenInList(
|
||||||
|
authInfo.tokens,
|
||||||
|
platform,
|
||||||
|
tokenValue,
|
||||||
|
);
|
||||||
|
if (!tokenExists && authInfo.token !== tokenValue) {
|
||||||
|
// Token not found, but don't throw error - user may have already logged out
|
||||||
|
this.logger.info(
|
||||||
|
`Logout attempted for non-existent token on platform: ${platform}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedTokens = this.removeTokenFromList(
|
||||||
|
authInfo.tokens,
|
||||||
|
platform,
|
||||||
|
tokenValue,
|
||||||
|
);
|
||||||
|
|
||||||
await this.updateAuthInfo(authInfo, {
|
await this.updateAuthInfo(authInfo, {
|
||||||
token: '',
|
token: authInfo.token === tokenValue ? '' : authInfo.token,
|
||||||
tokens: { ...authInfo.tokens, [platform]: '' },
|
tokens: updatedTokens,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +311,16 @@ 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) || '';
|
||||||
|
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, {
|
await this.updateAuthInfo(authInfo, {
|
||||||
lastip: ip,
|
lastip: ip,
|
||||||
lastaddr: address,
|
lastaddr: address,
|
||||||
@@ -346,13 +402,113 @@ export default class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeTokens(
|
||||||
|
tokens: Record<string, string | TokenInfo[]>,
|
||||||
|
): Record<string, TokenInfo[]> {
|
||||||
|
const normalized: Record<string, TokenInfo[]> = {};
|
||||||
|
|
||||||
|
for (const [platform, value] of Object.entries(tokens)) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
// Legacy format: convert string token to TokenInfo array
|
||||||
|
if (value) {
|
||||||
|
normalized[platform] = [
|
||||||
|
{
|
||||||
|
value,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
ip: '',
|
||||||
|
address: '',
|
||||||
|
platform,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
normalized[platform] = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Already in new format
|
||||||
|
normalized[platform] = value || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private addTokenToList(
|
||||||
|
tokens: Record<string, string | TokenInfo[]>,
|
||||||
|
platform: string,
|
||||||
|
tokenInfo: TokenInfo,
|
||||||
|
maxTokensPerPlatform: number = config.maxTokensPerPlatform,
|
||||||
|
): Record<string, TokenInfo[]> {
|
||||||
|
// Validate maxTokensPerPlatform parameter
|
||||||
|
if (!Number.isInteger(maxTokensPerPlatform) || maxTokensPerPlatform < 1) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Invalid maxTokensPerPlatform value: ${maxTokensPerPlatform}, using default`,
|
||||||
|
);
|
||||||
|
maxTokensPerPlatform = config.maxTokensPerPlatform;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = this.normalizeTokens(tokens);
|
||||||
|
|
||||||
|
if (!normalized[platform]) {
|
||||||
|
normalized[platform] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new token
|
||||||
|
normalized[platform].unshift(tokenInfo);
|
||||||
|
|
||||||
|
// Limit the number of active tokens per platform
|
||||||
|
if (normalized[platform].length > maxTokensPerPlatform) {
|
||||||
|
normalized[platform] = normalized[platform].slice(
|
||||||
|
0,
|
||||||
|
maxTokensPerPlatform,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeTokenFromList(
|
||||||
|
tokens: Record<string, string | TokenInfo[]>,
|
||||||
|
platform: string,
|
||||||
|
tokenValue: string,
|
||||||
|
): Record<string, TokenInfo[]> {
|
||||||
|
const normalized = this.normalizeTokens(tokens);
|
||||||
|
|
||||||
|
if (normalized[platform]) {
|
||||||
|
normalized[platform] = normalized[platform].filter(
|
||||||
|
(t) => t.value !== tokenValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private findTokenInList(
|
||||||
|
tokens: Record<string, string | TokenInfo[]>,
|
||||||
|
platform: string,
|
||||||
|
tokenValue: string,
|
||||||
|
): TokenInfo | undefined {
|
||||||
|
const normalized = this.normalizeTokens(tokens);
|
||||||
|
|
||||||
|
if (normalized[platform]) {
|
||||||
|
return normalized[platform].find((t) => t.value === tokenValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
public async resetAuthInfo(info: Partial<AuthInfo>) {
|
public async resetAuthInfo(info: Partial<AuthInfo>) {
|
||||||
const { retries, twoFactorActivated, password } = info;
|
const { retries, twoFactorActivated, password, username } = info;
|
||||||
const authInfo = await this.getAuthInfo();
|
const authInfo = await this.getAuthInfo();
|
||||||
await this.updateAuthInfo(authInfo, {
|
const payload = pickBy(
|
||||||
retries,
|
{
|
||||||
twoFactorActivated,
|
retries,
|
||||||
password,
|
twoFactorActivated,
|
||||||
});
|
password,
|
||||||
|
username,
|
||||||
|
},
|
||||||
|
(x) => !isNil(x),
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.updateAuthInfo(authInfo, payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { AuthInfo, TokenInfo } from '../data/system';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a token exists in the authentication info.
|
||||||
|
* Supports both legacy string tokens and new TokenInfo array format.
|
||||||
|
*
|
||||||
|
* @param authInfo - The authentication information
|
||||||
|
* @param headerToken - The token to validate
|
||||||
|
* @param platform - The platform (desktop, mobile)
|
||||||
|
* @returns true if the token is valid, false otherwise
|
||||||
|
*/
|
||||||
|
export function isValidToken(
|
||||||
|
authInfo: AuthInfo | null | undefined,
|
||||||
|
headerToken: string,
|
||||||
|
platform: string,
|
||||||
|
): boolean {
|
||||||
|
if (!authInfo || !headerToken) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { token = '', tokens = {} } = authInfo;
|
||||||
|
|
||||||
|
// Check legacy token field
|
||||||
|
if (headerToken === token) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check platform-specific tokens (support both legacy string and new TokenInfo[] format)
|
||||||
|
const platformTokens = tokens[platform];
|
||||||
|
|
||||||
|
// Handle null/undefined platformTokens
|
||||||
|
if (platformTokens === null || platformTokens === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof platformTokens === 'string') {
|
||||||
|
// Legacy format: single string token
|
||||||
|
return headerToken === platformTokens;
|
||||||
|
} else if (Array.isArray(platformTokens)) {
|
||||||
|
// New format: array of TokenInfo objects
|
||||||
|
return platformTokens.some((t: TokenInfo) => t && t.value === headerToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unexpected type - log warning and reject
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { createWriteStream, WriteStream } from 'fs';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages write streams for log files to improve performance by avoiding repeated file opens
|
||||||
|
*/
|
||||||
|
export class LogStreamManager extends EventEmitter {
|
||||||
|
private streams: Map<string, WriteStream> = new Map();
|
||||||
|
private pendingWrites: Map<string, Promise<void>> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write data to a log file using a managed stream
|
||||||
|
* @param filePath - Absolute path to the log file
|
||||||
|
* @param data - Data to write to the log file
|
||||||
|
*/
|
||||||
|
async write(filePath: string, data: string): Promise<void> {
|
||||||
|
// Wait for any pending writes to this file to complete
|
||||||
|
const pending = this.pendingWrites.get(filePath);
|
||||||
|
if (pending) {
|
||||||
|
await pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new promise for this write operation
|
||||||
|
const writePromise = new Promise<void>((resolve, reject) => {
|
||||||
|
let stream = this.streams.get(filePath);
|
||||||
|
|
||||||
|
if (!stream) {
|
||||||
|
// Create a new write stream if one doesn't exist
|
||||||
|
stream = createWriteStream(filePath, { flags: 'a' });
|
||||||
|
this.streams.set(filePath, stream);
|
||||||
|
|
||||||
|
// Handle stream errors
|
||||||
|
stream.on('error', (error) => {
|
||||||
|
this.emit('error', { filePath, error });
|
||||||
|
// Remove the stream from the map on error
|
||||||
|
this.streams.delete(filePath);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the data
|
||||||
|
const canContinue = stream.write(data, 'utf8', (error) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle backpressure
|
||||||
|
if (!canContinue) {
|
||||||
|
stream.once('drain', () => {
|
||||||
|
// Stream is ready for more data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pendingWrites.set(filePath, writePromise);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writePromise;
|
||||||
|
} finally {
|
||||||
|
this.pendingWrites.delete(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the stream for a specific file path
|
||||||
|
* @param filePath - Absolute path to the log file
|
||||||
|
*/
|
||||||
|
async closeStream(filePath: string): Promise<void> {
|
||||||
|
// Wait for any pending writes to complete
|
||||||
|
const pending = this.pendingWrites.get(filePath);
|
||||||
|
if (pending) {
|
||||||
|
await pending.catch(() => {
|
||||||
|
// Ignore errors on pending writes during close
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = this.streams.get(filePath);
|
||||||
|
if (stream) {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
stream.end(() => {
|
||||||
|
this.streams.delete(filePath);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close all open streams
|
||||||
|
*/
|
||||||
|
async closeAll(): Promise<void> {
|
||||||
|
const closePromises = Array.from(this.streams.keys()).map((filePath) =>
|
||||||
|
this.closeStream(filePath),
|
||||||
|
);
|
||||||
|
await Promise.all(closePromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of open streams
|
||||||
|
*/
|
||||||
|
getOpenStreamCount(): number {
|
||||||
|
return this.streams.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export a singleton instance for shared use
|
||||||
|
export const logStreamManager = new LogStreamManager();
|
||||||
+14
-1
@@ -37,7 +37,7 @@ class TaskLimit {
|
|||||||
concurrency: Math.max(os.cpus().length, 4),
|
concurrency: Math.max(os.cpus().length, 4),
|
||||||
});
|
});
|
||||||
private client = new ApiClient(
|
private client = new ApiClient(
|
||||||
`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 },
|
||||||
);
|
);
|
||||||
@@ -189,6 +189,19 @@ class TaskLimit {
|
|||||||
return this.scriptLimit.add(fn, options);
|
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>,
|
||||||
|
|||||||
@@ -2,10 +2,45 @@ 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';
|
import { ICron } from '../protos/cron';
|
||||||
|
import { CrontabModel, CrontabStatus } from '../data/cron';
|
||||||
|
import { killTask } from '../config/util';
|
||||||
|
|
||||||
export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
||||||
return taskLimit.runWithCronLimit(cron, () => {
|
return taskLimit.runWithCronLimit(cron, () => {
|
||||||
return new Promise(async (resolve: any) => {
|
return new Promise(async (resolve: any) => {
|
||||||
|
// Check if the cron is already running and stop it (only if multiple instances are not allowed)
|
||||||
|
try {
|
||||||
|
const existingCron = await CrontabModel.findOne({
|
||||||
|
where: { id: Number(cron.id) },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default to single instance mode (0) for backward compatibility
|
||||||
|
const allowSingleInstances =
|
||||||
|
existingCron?.allow_multiple_instances === 0;
|
||||||
|
|
||||||
|
if (
|
||||||
|
allowSingleInstances &&
|
||||||
|
existingCron &&
|
||||||
|
existingCron.pid &&
|
||||||
|
(existingCron.status === CrontabStatus.running ||
|
||||||
|
existingCron.status === CrontabStatus.queued)
|
||||||
|
) {
|
||||||
|
Logger.info(
|
||||||
|
`[schedule][停止已运行任务] 任务ID: ${cron.id}, PID: ${existingCron.pid}`,
|
||||||
|
);
|
||||||
|
await killTask(existingCron.pid);
|
||||||
|
// Update the status to idle after killing
|
||||||
|
await CrontabModel.update(
|
||||||
|
{ status: CrontabStatus.idle, pid: undefined },
|
||||||
|
{ where: { id: Number(cron.id) } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(
|
||||||
|
`[schedule][检查已运行任务失败] 任务ID: ${cron.id}, 错误: ${error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Logger.info(
|
Logger.info(
|
||||||
`[schedule][开始执行任务] 参数 ${JSON.stringify({
|
`[schedule][开始执行任务] 参数 ${JSON.stringify({
|
||||||
...cron,
|
...cron,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function getUniqueLockPath(filePath: string) {
|
|||||||
|
|
||||||
export async function writeFileWithLock(
|
export async function writeFileWithLock(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
content: string | Buffer,
|
content: string,
|
||||||
options: Parameters<typeof writeFile>[2] = {},
|
options: Parameters<typeof writeFile>[2] = {},
|
||||||
) {
|
) {
|
||||||
if (typeof options === 'string') {
|
if (typeof options === 'string') {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ 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';
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"typeRoots": ["./node_modules/celebrate/lib", "./node_modules/@types"],
|
"typeRoots": [
|
||||||
|
"./types",
|
||||||
|
"../node_modules/celebrate/lib",
|
||||||
|
"../node_modules/@types"
|
||||||
|
],
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
@@ -13,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/**/*", "./back.d.ts"],
|
"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();
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Joi } from 'celebrate';
|
import { Joi } from 'celebrate';
|
||||||
import cron_parser from 'cron-parser';
|
import { CronExpressionParser } from 'cron-parser';
|
||||||
import { ScheduleType } from '../interface/schedule';
|
import { ScheduleType } from '../interface/schedule';
|
||||||
|
import path from 'path';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
const validateSchedule = (value: string, helpers: any) => {
|
const validateSchedule = (value: string, helpers: any) => {
|
||||||
if (
|
if (
|
||||||
@@ -11,7 +13,7 @@ const validateSchedule = (value: string, helpers: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (cron_parser.parseExpression(value).hasNext()) {
|
if (CronExpressionParser.parse(value).hasNext()) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -37,4 +39,47 @@ export const commonCronSchema = {
|
|||||||
extra_schedules: Joi.array().optional().allow(null),
|
extra_schedules: Joi.array().optional().allow(null),
|
||||||
task_before: Joi.string().optional().allow('').allow(null),
|
task_before: Joi.string().optional().allow('').allow(null),
|
||||||
task_after: Joi.string().optional().allow('').allow(null),
|
task_after: Joi.string().optional().allow('').allow(null),
|
||||||
|
log_name: Joi.string()
|
||||||
|
.optional()
|
||||||
|
.allow('')
|
||||||
|
.allow(null)
|
||||||
|
.custom((value, helpers) => {
|
||||||
|
if (!value) return value;
|
||||||
|
|
||||||
|
// Check if it's an absolute path
|
||||||
|
if (value.startsWith('/')) {
|
||||||
|
// Allow /dev/null as special case
|
||||||
|
if (value === '/dev/null') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other absolute paths, ensure they are within the safe log directory
|
||||||
|
const normalizedValue = path.normalize(value);
|
||||||
|
const normalizedLogPath = path.normalize(config.logPath);
|
||||||
|
|
||||||
|
if (!normalizedValue.startsWith(normalizedLogPath)) {
|
||||||
|
return helpers.error('string.unsafePath');
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!/^(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?:\/)?(?:[\w.-]+\/)*[\w.-]+\/?$/.test(
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return helpers.error('string.pattern.base');
|
||||||
|
}
|
||||||
|
if (value.length > 100) {
|
||||||
|
return helpers.error('string.max');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
})
|
||||||
|
.messages({
|
||||||
|
'string.pattern.base': '日志名称只能包含字母、数字、下划线和连字符',
|
||||||
|
'string.max': '日志名称不能超过100个字符',
|
||||||
|
'string.unsafePath': '绝对路径必须在日志目录内或使用 /dev/null',
|
||||||
|
}),
|
||||||
|
allow_multiple_instances: Joi.number().optional().valid(0, 1).allow(null),
|
||||||
};
|
};
|
||||||
|
|||||||
+18
-13
@@ -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 \
|
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,14 +50,11 @@ 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
|
||||||
&& pip3 install requests
|
|
||||||
|
|
||||||
ARG SOURCE_COMMIT
|
ARG SOURCE_COMMIT
|
||||||
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||||
@@ -73,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
-13
@@ -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 \
|
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,14 +50,11 @@ 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
|
||||||
&& pip3 install requests
|
|
||||||
|
|
||||||
ARG SOURCE_COMMIT
|
ARG SOURCE_COMMIT
|
||||||
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||||
@@ -73,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"]
|
||||||
|
|||||||
+34
-22
@@ -1,44 +1,56 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
export PATH="$HOME/bin:$PATH"
|
||||||
|
|
||||||
dir_shell=/ql/shell
|
dir_shell=/ql/shell
|
||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
. $dir_shell/env.sh
|
|
||||||
|
|
||||||
echo -e "======================1. 检测配置文件========================\n"
|
export_ql_envs() {
|
||||||
|
export BACK_PORT="${ql_port}"
|
||||||
|
export GRPC_PORT="${ql_grpc_port}"
|
||||||
|
}
|
||||||
|
|
||||||
|
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}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fix DNS resolution issues in Alpine Linux
|
||||||
|
# Alpine uses musl libc which has known DNS resolver issues with certain domains
|
||||||
|
# Adding ndots:0 prevents unnecessary search domain appending
|
||||||
|
if [ -f /etc/alpine-release ]; then
|
||||||
|
if ! grep -q "^options ndots:0" /etc/resolv.conf 2>/dev/null; then
|
||||||
|
echo "options ndots:0" >> /etc/resolv.conf
|
||||||
|
log_with_style "INFO" "🔧 0. 已配置 DNS 解析优化 (ndots:0)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_with_style "INFO" "🚀 1. 检测配置文件..."
|
||||||
|
load_ql_envs
|
||||||
|
export_ql_envs
|
||||||
|
. $dir_shell/env.sh
|
||||||
import_config "$@"
|
import_config "$@"
|
||||||
make_dir /etc/nginx/conf.d
|
|
||||||
make_dir /run/nginx
|
|
||||||
init_nginx
|
|
||||||
fix_config
|
fix_config
|
||||||
|
|
||||||
pm2 l &>/dev/null
|
# Try to initialize PM2, but don't fail if it doesn't work
|
||||||
|
pm2 l &>/dev/null || log_with_style "WARN" "PM2 初始化可能失败,将在启动时尝试使用备用方案"
|
||||||
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
+13
-3
@@ -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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
+15
-19
@@ -1,20 +1,14 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"packageManager": "pnpm@8.3.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "concurrently -n w: npm:start:*",
|
"start": "concurrently -n w: npm:start:*",
|
||||||
"start:update": "ts-node -P tsconfig.back.json ./back/update.ts",
|
"start:back": "nodemon ./back/app.ts",
|
||||||
"start:public": "ts-node -P tsconfig.back.json ./back/public.ts",
|
|
||||||
"start:rpc": "ts-node -P tsconfig.back.json ./back/schedule/index.ts",
|
|
||||||
"start:back": "nodemon",
|
|
||||||
"start:front": "max dev",
|
"start:front": "max dev",
|
||||||
"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",
|
|
||||||
"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,snakeToCamel=false",
|
"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",
|
||||||
"gen:api": "python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. ./back/protos/api.proto",
|
|
||||||
"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",
|
||||||
@@ -61,14 +55,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "^1.12.3",
|
"@grpc/grpc-js": "^1.14.0",
|
||||||
"@grpc/proto-loader": "^0.7.13",
|
"@grpc/proto-loader": "^0.8.0",
|
||||||
"@otplib/preset-default": "^12.0.1",
|
"@otplib/preset-default": "^12.0.1",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
"celebrate": "^15.0.3",
|
"celebrate": "^15.0.3",
|
||||||
"chokidar": "^4.0.1",
|
"chokidar": "^4.0.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cron-parser": "^4.9.0",
|
"cron-parser": "^5.4.0",
|
||||||
"cross-spawn": "^7.0.6",
|
"cross-spawn": "^7.0.6",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dotenv": "^16.4.6",
|
"dotenv": "^16.4.6",
|
||||||
@@ -76,8 +70,7 @@
|
|||||||
"express-jwt": "^8.4.1",
|
"express-jwt": "^8.4.1",
|
||||||
"express-rate-limit": "^7.4.1",
|
"express-rate-limit": "^7.4.1",
|
||||||
"express-urlrewrite": "^2.0.3",
|
"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": "^3.0.3",
|
"http-proxy-middleware": "^3.0.3",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
@@ -88,11 +81,10 @@
|
|||||||
"node-schedule": "^2.1.0",
|
"node-schedule": "^2.1.0",
|
||||||
"nodemailer": "^6.9.16",
|
"nodemailer": "^6.9.16",
|
||||||
"p-queue-cjs": "7.3.4",
|
"p-queue-cjs": "7.3.4",
|
||||||
"@bufbuild/protobuf": "^2.2.3",
|
"@bufbuild/protobuf": "^2.10.0",
|
||||||
"pstree.remy": "^1.1.8",
|
"ps-tree": "^1.2.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"sequelize": "^6.37.5",
|
"sequelize": "^6.37.5",
|
||||||
"serve-handler": "^6.1.6",
|
|
||||||
"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": "^3.0.1",
|
"toad-scheduler": "^3.0.1",
|
||||||
@@ -104,7 +96,9 @@
|
|||||||
"ip2region": "2.3.0",
|
"ip2region": "2.3.0",
|
||||||
"keyv": "^5.2.3",
|
"keyv": "^5.2.3",
|
||||||
"@keyv/sqlite": "^4.0.1",
|
"@keyv/sqlite": "^4.0.1",
|
||||||
"proper-lockfile": "^4.1.2"
|
"proper-lockfile": "^4.1.2",
|
||||||
|
"compression": "^1.7.4",
|
||||||
|
"helmet": "^8.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
@@ -138,6 +132,7 @@
|
|||||||
"@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/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.4.4",
|
"@umijs/max": "^4.4.4",
|
||||||
@@ -176,6 +171,7 @@
|
|||||||
"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",
|
||||||
"yorkie": "^2.0.0"
|
"@types/compression": "^1.7.2",
|
||||||
|
"@types/helmet": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+131
-342
@@ -9,14 +9,14 @@ overrides:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@bufbuild/protobuf':
|
'@bufbuild/protobuf':
|
||||||
specifier: ^2.2.3
|
specifier: ^2.10.0
|
||||||
version: 2.2.3
|
version: 2.10.0
|
||||||
'@grpc/grpc-js':
|
'@grpc/grpc-js':
|
||||||
specifier: ^1.12.3
|
specifier: ^1.14.0
|
||||||
version: 1.12.3
|
version: 1.14.0
|
||||||
'@grpc/proto-loader':
|
'@grpc/proto-loader':
|
||||||
specifier: ^0.7.13
|
specifier: ^0.8.0
|
||||||
version: 0.7.13
|
version: 0.8.0
|
||||||
'@keyv/sqlite':
|
'@keyv/sqlite':
|
||||||
specifier: ^4.0.1
|
specifier: ^4.0.1
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
@@ -32,12 +32,15 @@ dependencies:
|
|||||||
chokidar:
|
chokidar:
|
||||||
specifier: ^4.0.1
|
specifier: ^4.0.1
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
|
compression:
|
||||||
|
specifier: ^1.7.4
|
||||||
|
version: 1.7.5
|
||||||
cors:
|
cors:
|
||||||
specifier: ^2.8.5
|
specifier: ^2.8.5
|
||||||
version: 2.8.5
|
version: 2.8.5
|
||||||
cron-parser:
|
cron-parser:
|
||||||
specifier: ^4.9.0
|
specifier: ^5.4.0
|
||||||
version: 4.9.0
|
version: 5.4.0
|
||||||
cross-spawn:
|
cross-spawn:
|
||||||
specifier: ^7.0.6
|
specifier: ^7.0.6
|
||||||
version: 7.0.6
|
version: 7.0.6
|
||||||
@@ -59,12 +62,9 @@ dependencies:
|
|||||||
express-urlrewrite:
|
express-urlrewrite:
|
||||||
specifier: ^2.0.3
|
specifier: ^2.0.3
|
||||||
version: 2.0.3
|
version: 2.0.3
|
||||||
form-data:
|
helmet:
|
||||||
specifier: ^4.0.0
|
specifier: ^8.1.0
|
||||||
version: 4.0.1
|
version: 8.1.0
|
||||||
got:
|
|
||||||
specifier: ^11.8.2
|
|
||||||
version: 11.8.6
|
|
||||||
hpagent:
|
hpagent:
|
||||||
specifier: ^1.2.0
|
specifier: ^1.2.0
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
@@ -104,9 +104,9 @@ dependencies:
|
|||||||
proper-lockfile:
|
proper-lockfile:
|
||||||
specifier: ^4.1.2
|
specifier: ^4.1.2
|
||||||
version: 4.1.2
|
version: 4.1.2
|
||||||
pstree.remy:
|
ps-tree:
|
||||||
specifier: ^1.1.8
|
specifier: ^1.2.0
|
||||||
version: 1.1.8
|
version: 1.2.0
|
||||||
reflect-metadata:
|
reflect-metadata:
|
||||||
specifier: ^0.2.2
|
specifier: ^0.2.2
|
||||||
version: 0.2.2
|
version: 0.2.2
|
||||||
@@ -116,9 +116,6 @@ dependencies:
|
|||||||
sequelize:
|
sequelize:
|
||||||
specifier: ^6.37.5
|
specifier: ^6.37.5
|
||||||
version: 6.37.5(@whyour/sqlite3@1.0.3)
|
version: 6.37.5(@whyour/sqlite3@1.0.3)
|
||||||
serve-handler:
|
|
||||||
specifier: ^6.1.6
|
|
||||||
version: 6.1.6
|
|
||||||
sockjs:
|
sockjs:
|
||||||
specifier: ^0.3.24
|
specifier: ^0.3.24
|
||||||
version: 0.3.24
|
version: 0.3.24
|
||||||
@@ -131,6 +128,9 @@ dependencies:
|
|||||||
typedi:
|
typedi:
|
||||||
specifier: ^0.10.0
|
specifier: ^0.10.0
|
||||||
version: 0.10.0
|
version: 0.10.0
|
||||||
|
undici:
|
||||||
|
specifier: ^7.9.0
|
||||||
|
version: 7.9.0
|
||||||
uuid:
|
uuid:
|
||||||
specifier: ^11.0.3
|
specifier: ^11.0.3
|
||||||
version: 11.0.3
|
version: 11.0.3
|
||||||
@@ -163,6 +163,9 @@ devDependencies:
|
|||||||
'@types/body-parser':
|
'@types/body-parser':
|
||||||
specifier: ^1.19.2
|
specifier: ^1.19.2
|
||||||
version: 1.19.5
|
version: 1.19.5
|
||||||
|
'@types/compression':
|
||||||
|
specifier: ^1.7.2
|
||||||
|
version: 1.7.5
|
||||||
'@types/cors':
|
'@types/cors':
|
||||||
specifier: ^2.8.12
|
specifier: ^2.8.12
|
||||||
version: 2.8.17
|
version: 2.8.17
|
||||||
@@ -178,6 +181,9 @@ devDependencies:
|
|||||||
'@types/file-saver':
|
'@types/file-saver':
|
||||||
specifier: 2.0.2
|
specifier: 2.0.2
|
||||||
version: 2.0.2
|
version: 2.0.2
|
||||||
|
'@types/helmet':
|
||||||
|
specifier: ^4.0.0
|
||||||
|
version: 4.0.0
|
||||||
'@types/js-yaml':
|
'@types/js-yaml':
|
||||||
specifier: ^4.0.5
|
specifier: ^4.0.5
|
||||||
version: 4.0.9
|
version: 4.0.9
|
||||||
@@ -202,6 +208,9 @@ devDependencies:
|
|||||||
'@types/proper-lockfile':
|
'@types/proper-lockfile':
|
||||||
specifier: ^4.1.4
|
specifier: ^4.1.4
|
||||||
version: 4.1.4
|
version: 4.1.4
|
||||||
|
'@types/ps-tree':
|
||||||
|
specifier: ^1.1.6
|
||||||
|
version: 1.1.6
|
||||||
'@types/qrcode.react':
|
'@types/qrcode.react':
|
||||||
specifier: ^1.0.2
|
specifier: ^1.0.2
|
||||||
version: 1.0.5
|
version: 1.0.5
|
||||||
@@ -349,9 +358,6 @@ devDependencies:
|
|||||||
virtualizedtableforantd4:
|
virtualizedtableforantd4:
|
||||||
specifier: 1.3.0
|
specifier: 1.3.0
|
||||||
version: 1.3.0(antd@4.24.16)(react-dom@18.3.1)(react@18.3.1)
|
version: 1.3.0(antd@4.24.16)(react-dom@18.3.1)(react@18.3.1)
|
||||||
yorkie:
|
|
||||||
specifier: ^2.0.0
|
|
||||||
version: 2.0.0
|
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -1380,8 +1386,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-h0OYmPR3A5Dfbetra/GzxBAzQk8sH7LhRkRUTdagX6nrtlUgJGYCTv4bBK33jsTQw9HDd8PE2x1Ma+iRKEDUsw==}
|
resolution: {integrity: sha512-h0OYmPR3A5Dfbetra/GzxBAzQk8sH7LhRkRUTdagX6nrtlUgJGYCTv4bBK33jsTQw9HDd8PE2x1Ma+iRKEDUsw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@bufbuild/protobuf@2.2.3:
|
/@bufbuild/protobuf@2.10.0:
|
||||||
resolution: {integrity: sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==}
|
resolution: {integrity: sha512-fdRs9PSrBF7QUntpZpq6BTw58fhgGJojgg39m9oFOJGZT+nip9b0so5cYY1oWl5pvemDLr0cPPsH46vwThEbpQ==}
|
||||||
|
|
||||||
/@chenshuai2144/sketch-color@1.0.9(react@18.3.1):
|
/@chenshuai2144/sketch-color@1.0.9(react@18.3.1):
|
||||||
resolution: {integrity: sha512-obzSy26cb7Pm7OprWyVpgMpIlrZpZ0B7vbrU0RMbvRg0YAI890S5Xy02Aj1Nhl4+KTbi1lVYHt6HQP8Hm9s+1w==}
|
resolution: {integrity: sha512-obzSy26cb7Pm7OprWyVpgMpIlrZpZ0B7vbrU0RMbvRg0YAI890S5Xy02Aj1Nhl4+KTbi1lVYHt6HQP8Hm9s+1w==}
|
||||||
@@ -2576,22 +2582,22 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@grpc/grpc-js@1.12.3:
|
/@grpc/grpc-js@1.14.0:
|
||||||
resolution: {integrity: sha512-iaxAZnANdCwMNpJlyhkI1W1jQZIDZKFNtU2OpQDdgd+pBcU3t7G+PT7svobkW4WSZTdis+CVV6y8KIwu83HDYQ==}
|
resolution: {integrity: sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg==}
|
||||||
engines: {node: '>=12.10.0'}
|
engines: {node: '>=12.10.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@grpc/proto-loader': 0.7.13
|
'@grpc/proto-loader': 0.8.0
|
||||||
'@js-sdsl/ordered-map': 4.4.2
|
'@js-sdsl/ordered-map': 4.4.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@grpc/proto-loader@0.7.13:
|
/@grpc/proto-loader@0.8.0:
|
||||||
resolution: {integrity: sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==}
|
resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash.camelcase: 4.3.0
|
lodash.camelcase: 4.3.0
|
||||||
long: 5.2.3
|
long: 5.2.3
|
||||||
protobufjs: 7.4.0
|
protobufjs: 7.5.4
|
||||||
yargs: 17.7.2
|
yargs: 17.7.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@@ -3485,11 +3491,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@sindresorhus/is@4.6.0:
|
|
||||||
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@stylelint/postcss-css-in-js@0.38.0(postcss-syntax@0.36.2)(postcss@8.4.49):
|
/@stylelint/postcss-css-in-js@0.38.0(postcss-syntax@0.36.2)(postcss@8.4.49):
|
||||||
resolution: {integrity: sha512-XOz5CAe49kS95p5yRd+DAIWDojTjfmyAQ4bbDlXMdbZTQ5t0ThjSLvWI6JI2uiS7MFurVBkZ6zUqcimzcLTBoQ==}
|
resolution: {integrity: sha512-XOz5CAe49kS95p5yRd+DAIWDojTjfmyAQ4bbDlXMdbZTQ5t0ThjSLvWI6JI2uiS7MFurVBkZ6zUqcimzcLTBoQ==}
|
||||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
@@ -3677,13 +3678,6 @@ packages:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@szmarczak/http-timer@4.0.6:
|
|
||||||
resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
dependencies:
|
|
||||||
defer-to-connect: 2.0.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@tanstack/match-sorter-utils@8.19.4:
|
/@tanstack/match-sorter-utils@8.19.4:
|
||||||
resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==}
|
resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -3799,14 +3793,11 @@ packages:
|
|||||||
'@types/node': 17.0.45
|
'@types/node': 17.0.45
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/cacheable-request@6.0.3:
|
/@types/compression@1.7.5:
|
||||||
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
|
resolution: {integrity: sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/http-cache-semantics': 4.0.4
|
'@types/express': 4.17.21
|
||||||
'@types/keyv': 3.1.4
|
dev: true
|
||||||
'@types/node': 17.0.45
|
|
||||||
'@types/responselike': 1.0.3
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@types/connect@3.4.38:
|
/@types/connect@3.4.38:
|
||||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||||
@@ -3878,6 +3869,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-oOMFT8vmCTFncsF1engrs04jatz8/Anwx3De9uxnOK4chgSEgWBvFtpSoJo8u3784JNO+ql5tzRR6phHoRnscQ==}
|
resolution: {integrity: sha512-oOMFT8vmCTFncsF1engrs04jatz8/Anwx3De9uxnOK4chgSEgWBvFtpSoJo8u3784JNO+ql5tzRR6phHoRnscQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/helmet@4.0.0:
|
||||||
|
resolution: {integrity: sha512-ONIn/nSNQA57yRge3oaMQESef/6QhoeX7llWeDli0UZIfz8TQMkfNPTXA8VnnyeA1WUjG2pGqdjEIueYonMdfQ==}
|
||||||
|
deprecated: This is a stub types definition. helmet provides its own type definitions, so you do not need this installed.
|
||||||
|
dependencies:
|
||||||
|
helmet: 8.1.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/hoist-non-react-statics@3.3.5:
|
/@types/hoist-non-react-statics@3.3.5:
|
||||||
resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==}
|
resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3889,10 +3887,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==}
|
resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/http-cache-semantics@4.0.4:
|
|
||||||
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@types/http-errors@2.0.4:
|
/@types/http-errors@2.0.4:
|
||||||
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
|
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -3950,12 +3944,6 @@ packages:
|
|||||||
'@types/node': 17.0.45
|
'@types/node': 17.0.45
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@types/keyv@3.1.4:
|
|
||||||
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 17.0.45
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@types/lodash@4.17.13:
|
/@types/lodash@4.17.13:
|
||||||
resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==}
|
resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -4011,6 +3999,10 @@ packages:
|
|||||||
'@types/retry': 0.12.5
|
'@types/retry': 0.12.5
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/ps-tree@1.1.6:
|
||||||
|
resolution: {integrity: sha512-PtrlVaOaI44/3pl3cvnlK+GxOM3re2526TJvPvh7W+keHIXdV4TE0ylpPBAcvFQCbGitaTXwL9u+RF7qtVeazQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/qrcode.react@1.0.5:
|
/@types/qrcode.react@1.0.5:
|
||||||
resolution: {integrity: sha512-BghPtnlwvrvq8QkGa1H25YnN+5OIgCKFuQruncGWLGJYOzeSKiix/4+B9BtfKF2wf5ja8yfyWYA3OXju995G8w==}
|
resolution: {integrity: sha512-BghPtnlwvrvq8QkGa1H25YnN+5OIgCKFuQruncGWLGJYOzeSKiix/4+B9BtfKF2wf5ja8yfyWYA3OXju995G8w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4054,12 +4046,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==}
|
resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/responselike@1.0.3:
|
|
||||||
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 17.0.45
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@types/retry@0.12.5:
|
/@types/retry@0.12.5:
|
||||||
resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==}
|
resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -5544,6 +5530,7 @@ packages:
|
|||||||
|
|
||||||
/asynckit@0.4.0:
|
/asynckit@0.4.0:
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/atomic-sleep@1.0.0:
|
/atomic-sleep@1.0.0:
|
||||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||||
@@ -5972,11 +5959,6 @@ packages:
|
|||||||
streamsearch: 1.1.0
|
streamsearch: 1.1.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/bytes@3.0.0:
|
|
||||||
resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==}
|
|
||||||
engines: {node: '>= 0.8'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/bytes@3.1.2:
|
/bytes@3.1.2:
|
||||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -6009,24 +5991,6 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/cacheable-lookup@5.0.4:
|
|
||||||
resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==}
|
|
||||||
engines: {node: '>=10.6.0'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/cacheable-request@7.0.4:
|
|
||||||
resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
dependencies:
|
|
||||||
clone-response: 1.0.3
|
|
||||||
get-stream: 5.2.0
|
|
||||||
http-cache-semantics: 4.1.1
|
|
||||||
keyv: 4.5.4
|
|
||||||
lowercase-keys: 2.0.0
|
|
||||||
normalize-url: 6.1.0
|
|
||||||
responselike: 2.0.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/call-bind@1.0.7:
|
/call-bind@1.0.7:
|
||||||
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
|
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -6144,10 +6108,6 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/ci-info@1.6.0:
|
|
||||||
resolution: {integrity: sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/ci-info@3.9.0:
|
/ci-info@3.9.0:
|
||||||
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
|
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -6230,12 +6190,6 @@ packages:
|
|||||||
is-regexp: 2.1.0
|
is-regexp: 2.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/clone-response@1.0.3:
|
|
||||||
resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==}
|
|
||||||
dependencies:
|
|
||||||
mimic-response: 1.0.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/codemirror-lang-mermaid@0.5.0:
|
/codemirror-lang-mermaid@0.5.0:
|
||||||
resolution: {integrity: sha512-Taw/2gPCyNArQJCxIP/HSUif+3zrvD+6Ugt7KJZ2dUKou/8r3ZhcfG8krNTZfV2iu8AuGnymKuo7bLPFyqsh/A==}
|
resolution: {integrity: sha512-Taw/2gPCyNArQJCxIP/HSUif+3zrvD+6Ugt7KJZ2dUKou/8r3ZhcfG8krNTZfV2iu8AuGnymKuo7bLPFyqsh/A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6314,6 +6268,7 @@ packages:
|
|||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream: 1.0.0
|
delayed-stream: 1.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/commander@11.0.0:
|
/commander@11.0.0:
|
||||||
resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==}
|
resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==}
|
||||||
@@ -6343,7 +6298,6 @@ packages:
|
|||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-db: 1.53.0
|
mime-db: 1.53.0
|
||||||
dev: true
|
|
||||||
|
|
||||||
/compression-webpack-plugin@9.2.0:
|
/compression-webpack-plugin@9.2.0:
|
||||||
resolution: {integrity: sha512-R/Oi+2+UHotGfu72fJiRoVpuRifZT0tTC6UqFD/DUo+mv8dbOow9rVOuTvDv5nPPm3GZhHL/fKkwxwIHnJ8Nyw==}
|
resolution: {integrity: sha512-R/Oi+2+UHotGfu72fJiRoVpuRifZT0tTC6UqFD/DUo+mv8dbOow9rVOuTvDv5nPPm3GZhHL/fKkwxwIHnJ8Nyw==}
|
||||||
@@ -6371,7 +6325,6 @@ packages:
|
|||||||
vary: 1.1.2
|
vary: 1.1.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/compute-scroll-into-view@1.0.20:
|
/compute-scroll-into-view@1.0.20:
|
||||||
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
|
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
|
||||||
@@ -6423,11 +6376,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==}
|
resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/content-disposition@0.5.2:
|
|
||||||
resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/content-disposition@0.5.4:
|
/content-disposition@0.5.4:
|
||||||
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -6576,19 +6524,18 @@ packages:
|
|||||||
luxon: 3.5.0
|
luxon: 3.5.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/cron-parser@5.4.0:
|
||||||
|
resolution: {integrity: sha512-HxYB8vTvnQFx4dLsZpGRa0uHp6X3qIzS3ZJgJ9v6l/5TJMgeWQbLkR5yiJ5hOxGbc9+jCADDnydIe15ReLZnJA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
dependencies:
|
||||||
|
luxon: 3.7.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/croner@7.0.8:
|
/croner@7.0.8:
|
||||||
resolution: {integrity: sha512-4E27J9ZQV9prM9ggU18QGPYPMSblbA9JuGv4Ff3Gk6supX4RszNGQxBgiFBL6wb/L9HuSMpFbQpduMiDRo+z5Q==}
|
resolution: {integrity: sha512-4E27J9ZQV9prM9ggU18QGPYPMSblbA9JuGv4Ff3Gk6supX4RszNGQxBgiFBL6wb/L9HuSMpFbQpduMiDRo+z5Q==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/cross-spawn@5.1.0:
|
|
||||||
resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
|
|
||||||
dependencies:
|
|
||||||
lru-cache: 4.1.5
|
|
||||||
shebang-command: 1.2.0
|
|
||||||
which: 1.3.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/cross-spawn@7.0.6:
|
/cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -6866,13 +6813,6 @@ packages:
|
|||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/decompress-response@6.0.0:
|
|
||||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
dependencies:
|
|
||||||
mimic-response: 3.1.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/deep-is@0.1.4:
|
/deep-is@0.1.4:
|
||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -6912,11 +6852,6 @@ packages:
|
|||||||
os-name: 1.0.3
|
os-name: 1.0.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/defer-to-connect@2.0.1:
|
|
||||||
resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/define-data-property@1.1.4:
|
/define-data-property@1.1.4:
|
||||||
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -6947,6 +6882,7 @@ packages:
|
|||||||
/delayed-stream@1.0.0:
|
/delayed-stream@1.0.0:
|
||||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/delegates@1.0.0:
|
/delegates@1.0.0:
|
||||||
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
||||||
@@ -7107,6 +7043,10 @@ packages:
|
|||||||
detect-libc: 1.0.3
|
detect-libc: 1.0.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/duplexer@0.1.2:
|
||||||
|
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/duplexify@4.1.3:
|
/duplexify@4.1.3:
|
||||||
resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==}
|
resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7233,6 +7173,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
|
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
|
||||||
dependencies:
|
dependencies:
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/enhanced-resolve@5.17.1:
|
/enhanced-resolve@5.17.1:
|
||||||
resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==}
|
resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==}
|
||||||
@@ -7755,6 +7696,18 @@ packages:
|
|||||||
es5-ext: 0.10.64
|
es5-ext: 0.10.64
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/event-stream@3.3.4:
|
||||||
|
resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==}
|
||||||
|
dependencies:
|
||||||
|
duplexer: 0.1.2
|
||||||
|
from: 0.1.7
|
||||||
|
map-stream: 0.1.0
|
||||||
|
pause-stream: 0.0.11
|
||||||
|
split: 0.3.3
|
||||||
|
stream-combiner: 0.0.4
|
||||||
|
through: 2.3.8
|
||||||
|
dev: false
|
||||||
|
|
||||||
/eventemitter3@4.0.7:
|
/eventemitter3@4.0.7:
|
||||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -7785,19 +7738,6 @@ packages:
|
|||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/execa@0.8.0:
|
|
||||||
resolution: {integrity: sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==}
|
|
||||||
engines: {node: '>=4'}
|
|
||||||
dependencies:
|
|
||||||
cross-spawn: 5.1.0
|
|
||||||
get-stream: 3.0.0
|
|
||||||
is-stream: 1.1.0
|
|
||||||
npm-run-path: 2.0.2
|
|
||||||
p-finally: 1.0.0
|
|
||||||
signal-exit: 3.0.7
|
|
||||||
strip-eof: 1.0.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/execa@5.1.1:
|
/execa@5.1.1:
|
||||||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -8157,6 +8097,7 @@ packages:
|
|||||||
asynckit: 0.4.0
|
asynckit: 0.4.0
|
||||||
combined-stream: 1.0.8
|
combined-stream: 1.0.8
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
|
dev: true
|
||||||
|
|
||||||
/formdata-polyfill@4.0.10:
|
/formdata-polyfill@4.0.10:
|
||||||
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||||
@@ -8186,6 +8127,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
/from@0.1.7:
|
||||||
|
resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/fs-extra@10.1.0:
|
/fs-extra@10.1.0:
|
||||||
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
|
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -8297,18 +8242,6 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/get-stream@3.0.0:
|
|
||||||
resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==}
|
|
||||||
engines: {node: '>=4'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/get-stream@5.2.0:
|
|
||||||
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
dependencies:
|
|
||||||
pump: 3.0.2
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/get-stream@6.0.1:
|
/get-stream@6.0.1:
|
||||||
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
|
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -8452,23 +8385,6 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic: 1.2.4
|
get-intrinsic: 1.2.4
|
||||||
|
|
||||||
/got@11.8.6:
|
|
||||||
resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==}
|
|
||||||
engines: {node: '>=10.19.0'}
|
|
||||||
dependencies:
|
|
||||||
'@sindresorhus/is': 4.6.0
|
|
||||||
'@szmarczak/http-timer': 4.0.6
|
|
||||||
'@types/cacheable-request': 6.0.3
|
|
||||||
'@types/responselike': 1.0.3
|
|
||||||
cacheable-lookup: 5.0.4
|
|
||||||
cacheable-request: 7.0.4
|
|
||||||
decompress-response: 6.0.0
|
|
||||||
http2-wrapper: 1.0.3
|
|
||||||
lowercase-keys: 2.0.0
|
|
||||||
p-cancelable: 2.1.1
|
|
||||||
responselike: 2.0.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/graceful-fs@4.2.11:
|
/graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
@@ -8558,6 +8474,10 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/helmet@8.1.0:
|
||||||
|
resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
/history@5.3.0:
|
/history@5.3.0:
|
||||||
resolution: {integrity: sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==}
|
resolution: {integrity: sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8657,7 +8577,9 @@ packages:
|
|||||||
|
|
||||||
/http-cache-semantics@4.1.1:
|
/http-cache-semantics@4.1.1:
|
||||||
resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
|
resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
|
||||||
|
requiresBuild: true
|
||||||
dev: false
|
dev: false
|
||||||
|
optional: true
|
||||||
|
|
||||||
/http-deceiver@1.2.7:
|
/http-deceiver@1.2.7:
|
||||||
resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==}
|
resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==}
|
||||||
@@ -8714,14 +8636,6 @@ packages:
|
|||||||
- debug
|
- debug
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/http2-wrapper@1.0.3:
|
|
||||||
resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==}
|
|
||||||
engines: {node: '>=10.19.0'}
|
|
||||||
dependencies:
|
|
||||||
quick-lru: 5.1.1
|
|
||||||
resolve-alpn: 1.2.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/https-browserify@1.0.0:
|
/https-browserify@1.0.0:
|
||||||
resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==}
|
resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -8995,13 +8909,6 @@ packages:
|
|||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/is-ci@1.2.1:
|
|
||||||
resolution: {integrity: sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==}
|
|
||||||
hasBin: true
|
|
||||||
dependencies:
|
|
||||||
ci-info: 1.6.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/is-core-module@2.15.1:
|
/is-core-module@2.15.1:
|
||||||
resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==}
|
resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -9190,11 +9097,6 @@ packages:
|
|||||||
call-bind: 1.0.7
|
call-bind: 1.0.7
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/is-stream@1.1.0:
|
|
||||||
resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/is-stream@2.0.1:
|
/is-stream@2.0.1:
|
||||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -9443,6 +9345,7 @@ packages:
|
|||||||
|
|
||||||
/json-buffer@3.0.1:
|
/json-buffer@3.0.1:
|
||||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/json-parse-even-better-errors@2.3.1:
|
/json-parse-even-better-errors@2.3.1:
|
||||||
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
||||||
@@ -9525,6 +9428,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
json-buffer: 3.0.1
|
json-buffer: 3.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/keyv@5.2.3:
|
/keyv@5.2.3:
|
||||||
resolution: {integrity: sha512-AGKecUfzrowabUv0bH1RIR5Vf7w+l4S3xtQAypKaUpTdIR1EbrAcTxHCrpo9Q+IWeUlFE2palRtgIQcgm+PQJw==}
|
resolution: {integrity: sha512-AGKecUfzrowabUv0bH1RIR5Vf7w+l4S3xtQAypKaUpTdIR1EbrAcTxHCrpo9Q+IWeUlFE2palRtgIQcgm+PQJw==}
|
||||||
@@ -9874,22 +9778,10 @@ packages:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/lowercase-keys@2.0.0:
|
|
||||||
resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/lru-cache@10.4.3:
|
/lru-cache@10.4.3:
|
||||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/lru-cache@4.1.5:
|
|
||||||
resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==}
|
|
||||||
dependencies:
|
|
||||||
pseudomap: 1.0.2
|
|
||||||
yallist: 2.1.2
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/lru-cache@5.1.1:
|
/lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9907,6 +9799,11 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/luxon@3.7.2:
|
||||||
|
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/make-dir@2.1.0:
|
/make-dir@2.1.0:
|
||||||
resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
|
resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -9971,6 +9868,10 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/map-stream@0.1.0:
|
||||||
|
resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/mathml-tag-names@2.1.3:
|
/mathml-tag-names@2.1.3:
|
||||||
resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
|
resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -10059,11 +9960,6 @@ packages:
|
|||||||
brorand: 1.1.0
|
brorand: 1.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/mime-db@1.33.0:
|
|
||||||
resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/mime-db@1.52.0:
|
/mime-db@1.52.0:
|
||||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -10071,14 +9967,6 @@ packages:
|
|||||||
/mime-db@1.53.0:
|
/mime-db@1.53.0:
|
||||||
resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==}
|
resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/mime-types@2.1.18:
|
|
||||||
resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
dependencies:
|
|
||||||
mime-db: 1.33.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/mime-types@2.1.35:
|
/mime-types@2.1.35:
|
||||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||||
@@ -10107,16 +9995,6 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/mimic-response@1.0.1:
|
|
||||||
resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
|
|
||||||
engines: {node: '>=4'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/mimic-response@3.1.0:
|
|
||||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/min-document@2.19.0:
|
/min-document@2.19.0:
|
||||||
resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==}
|
resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -10521,11 +10399,6 @@ packages:
|
|||||||
validate-npm-package-license: 3.0.4
|
validate-npm-package-license: 3.0.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/normalize-path@1.0.0:
|
|
||||||
resolution: {integrity: sha512-7WyT0w8jhpDStXRq5836AMmihQwq2nrUVQrgjvUo/p/NZf9uy/MeJ246lBJVmWuYXMlJuG9BNZHF0hWjfTbQUA==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/normalize-path@3.0.0:
|
/normalize-path@3.0.0:
|
||||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -10540,22 +10413,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-dxvWdI8gw6eAvk9BlPffgEoGfM7AdijoCwOEJge3e3ulT2XLgmU7KvvxprOaCu05Q1uGRHmOhHe1r6emZoKyFw==}
|
resolution: {integrity: sha512-dxvWdI8gw6eAvk9BlPffgEoGfM7AdijoCwOEJge3e3ulT2XLgmU7KvvxprOaCu05Q1uGRHmOhHe1r6emZoKyFw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/normalize-url@6.1.0:
|
|
||||||
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/normalize-wheel@1.0.1:
|
/normalize-wheel@1.0.1:
|
||||||
resolution: {integrity: sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==}
|
resolution: {integrity: sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/npm-run-path@2.0.2:
|
|
||||||
resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==}
|
|
||||||
engines: {node: '>=4'}
|
|
||||||
dependencies:
|
|
||||||
path-key: 2.0.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/npm-run-path@4.0.1:
|
/npm-run-path@4.0.1:
|
||||||
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -10695,7 +10556,6 @@ packages:
|
|||||||
/on-headers@1.0.2:
|
/on-headers@1.0.2:
|
||||||
resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
|
resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/once@1.4.0:
|
/once@1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
@@ -10774,16 +10634,6 @@ packages:
|
|||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/p-cancelable@2.1.1:
|
|
||||||
resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/p-finally@1.0.0:
|
|
||||||
resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
|
|
||||||
engines: {node: '>=4'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/p-limit@2.3.0:
|
/p-limit@2.3.0:
|
||||||
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -10913,15 +10763,6 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
|
|
||||||
/path-is-inside@1.0.2:
|
|
||||||
resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/path-key@2.0.1:
|
|
||||||
resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==}
|
|
||||||
engines: {node: '>=4'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/path-key@3.1.1:
|
/path-key@3.1.1:
|
||||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -10956,10 +10797,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==}
|
resolution: {integrity: sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/path-to-regexp@3.3.0:
|
|
||||||
resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/path-to-regexp@6.3.0:
|
/path-to-regexp@6.3.0:
|
||||||
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
|
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -10978,7 +10815,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==}
|
resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
through: 2.3.8
|
through: 2.3.8
|
||||||
dev: true
|
|
||||||
|
|
||||||
/pbkdf2@3.1.2:
|
/pbkdf2@3.1.2:
|
||||||
resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==}
|
resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==}
|
||||||
@@ -11746,8 +11582,8 @@ packages:
|
|||||||
signal-exit: 3.0.7
|
signal-exit: 3.0.7
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/protobufjs@7.4.0:
|
/protobufjs@7.5.4:
|
||||||
resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==}
|
resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -11786,12 +11622,17 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/pseudomap@1.0.2:
|
/ps-tree@1.2.0:
|
||||||
resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==}
|
resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==}
|
||||||
dev: true
|
engines: {node: '>= 0.10'}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
event-stream: 3.3.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
/pstree.remy@1.1.8:
|
/pstree.remy@1.1.8:
|
||||||
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
|
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/public-encrypt@4.0.3:
|
/public-encrypt@4.0.3:
|
||||||
resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==}
|
resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==}
|
||||||
@@ -11809,6 +11650,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
end-of-stream: 1.4.4
|
end-of-stream: 1.4.4
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/punycode-okam@1.4.1:
|
/punycode-okam@1.4.1:
|
||||||
resolution: {integrity: sha512-e4mSfzGfrVBJmhjp+8PHjXIz5WrvEEWB2FT+RJ6YS/ozGttTcnocuj0CtMo3dujWYe2708bTd79zeIrKBtRzCg==}
|
resolution: {integrity: sha512-e4mSfzGfrVBJmhjp+8PHjXIz5WrvEEWB2FT+RJ6YS/ozGttTcnocuj0CtMo3dujWYe2708bTd79zeIrKBtRzCg==}
|
||||||
@@ -11926,11 +11768,6 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/quick-lru@5.1.1:
|
|
||||||
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/raf@3.4.1:
|
/raf@3.4.1:
|
||||||
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
|
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -11950,11 +11787,6 @@ packages:
|
|||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/range-parser@1.2.0:
|
|
||||||
resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/range-parser@1.2.1:
|
/range-parser@1.2.1:
|
||||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -13253,10 +13085,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/resolve-alpn@1.2.1:
|
|
||||||
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/resolve-from@4.0.0:
|
/resolve-from@4.0.0:
|
||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -13289,12 +13117,6 @@ packages:
|
|||||||
supports-preserve-symlinks-flag: 1.0.0
|
supports-preserve-symlinks-flag: 1.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/responselike@2.0.1:
|
|
||||||
resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==}
|
|
||||||
dependencies:
|
|
||||||
lowercase-keys: 2.0.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/restore-cursor@4.0.0:
|
/restore-cursor@4.0.0:
|
||||||
resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
|
resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
@@ -13564,18 +13386,6 @@ packages:
|
|||||||
randombytes: 2.1.0
|
randombytes: 2.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/serve-handler@6.1.6:
|
|
||||||
resolution: {integrity: sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==}
|
|
||||||
dependencies:
|
|
||||||
bytes: 3.0.0
|
|
||||||
content-disposition: 0.5.2
|
|
||||||
mime-types: 2.1.18
|
|
||||||
minimatch: 3.1.2
|
|
||||||
path-is-inside: 1.0.2
|
|
||||||
path-to-regexp: 3.3.0
|
|
||||||
range-parser: 1.2.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/serve-static@1.16.2:
|
/serve-static@1.16.2:
|
||||||
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
|
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -13635,24 +13445,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
|
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/shebang-command@1.2.0:
|
|
||||||
resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
dependencies:
|
|
||||||
shebang-regex: 1.0.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/shebang-command@2.0.0:
|
/shebang-command@2.0.0:
|
||||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dependencies:
|
dependencies:
|
||||||
shebang-regex: 3.0.0
|
shebang-regex: 3.0.0
|
||||||
|
|
||||||
/shebang-regex@1.0.0:
|
|
||||||
resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/shebang-regex@3.0.0:
|
/shebang-regex@3.0.0:
|
||||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -13894,6 +13692,12 @@ packages:
|
|||||||
engines: {node: '>= 10.x'}
|
engines: {node: '>= 10.x'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/split@0.3.3:
|
||||||
|
resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==}
|
||||||
|
dependencies:
|
||||||
|
through: 2.3.8
|
||||||
|
dev: false
|
||||||
|
|
||||||
/sprintf-js@1.0.3:
|
/sprintf-js@1.0.3:
|
||||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -13953,6 +13757,12 @@ packages:
|
|||||||
readable-stream: 2.3.8
|
readable-stream: 2.3.8
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/stream-combiner@0.0.4:
|
||||||
|
resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==}
|
||||||
|
dependencies:
|
||||||
|
duplexer: 0.1.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/stream-http@2.8.3:
|
/stream-http@2.8.3:
|
||||||
resolution: {integrity: sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==}
|
resolution: {integrity: sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -14077,11 +13887,6 @@ packages:
|
|||||||
ansi-regex: 6.1.0
|
ansi-regex: 6.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/strip-eof@1.0.0:
|
|
||||||
resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/strip-final-newline@2.0.0:
|
/strip-final-newline@2.0.0:
|
||||||
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
|
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -14092,11 +13897,6 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/strip-indent@2.0.0:
|
|
||||||
resolution: {integrity: sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==}
|
|
||||||
engines: {node: '>=4'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/strip-indent@3.0.0:
|
/strip-indent@3.0.0:
|
||||||
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
|
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -14420,7 +14220,6 @@ packages:
|
|||||||
|
|
||||||
/through@2.3.8:
|
/through@2.3.8:
|
||||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/timers-browserify@2.0.12:
|
/timers-browserify@2.0.12:
|
||||||
resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==}
|
resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==}
|
||||||
@@ -14546,14 +14345,14 @@ packages:
|
|||||||
/ts-proto-descriptors@2.0.0:
|
/ts-proto-descriptors@2.0.0:
|
||||||
resolution: {integrity: sha512-wHcTH3xIv11jxgkX5OyCSFfw27agpInAd6yh89hKG6zqIXnjW9SYqSER2CVQxdPj4czeOhGagNvZBEbJPy7qkw==}
|
resolution: {integrity: sha512-wHcTH3xIv11jxgkX5OyCSFfw27agpInAd6yh89hKG6zqIXnjW9SYqSER2CVQxdPj4czeOhGagNvZBEbJPy7qkw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@bufbuild/protobuf': 2.2.3
|
'@bufbuild/protobuf': 2.10.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/ts-proto@2.6.1:
|
/ts-proto@2.6.1:
|
||||||
resolution: {integrity: sha512-4LTT99MkwkF1+fIA0b2mZu/58Qlpq3Q1g53TwEMZZgR1w/uX00PoVT4Z8aKJxMw0LeKQD4s9NrJYsF27Clckrg==}
|
resolution: {integrity: sha512-4LTT99MkwkF1+fIA0b2mZu/58Qlpq3Q1g53TwEMZZgR1w/uX00PoVT4Z8aKJxMw0LeKQD4s9NrJYsF27Clckrg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@bufbuild/protobuf': 2.2.3
|
'@bufbuild/protobuf': 2.10.0
|
||||||
case-anything: 2.1.13
|
case-anything: 2.1.13
|
||||||
ts-poet: 6.9.0
|
ts-poet: 6.9.0
|
||||||
ts-proto-descriptors: 2.0.0
|
ts-proto-descriptors: 2.0.0
|
||||||
@@ -14785,6 +14584,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/undici@7.9.0:
|
||||||
|
resolution: {integrity: sha512-e696y354tf5cFZPXsF26Yg+5M63+5H3oE6Vtkh2oqbvsE2Oe7s2nIbcQh5lmG7Lp/eS29vJtTpw9+p6PX0qNSg==}
|
||||||
|
engines: {node: '>=20.18.1'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/unescape@1.0.1:
|
/unescape@1.0.1:
|
||||||
resolution: {integrity: sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==}
|
resolution: {integrity: sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -15341,10 +15145,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
/yallist@2.1.2:
|
|
||||||
resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/yallist@3.1.1:
|
/yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -15393,17 +15193,6 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/yorkie@2.0.0:
|
|
||||||
resolution: {integrity: sha512-jcKpkthap6x63MB4TxwCyuIGkV0oYP/YRyuQU5UO0Yz/E/ZAu+653/uov+phdmO54n6BcvFRyyt0RRrWdN2mpw==}
|
|
||||||
engines: {node: '>=4'}
|
|
||||||
requiresBuild: true
|
|
||||||
dependencies:
|
|
||||||
execa: 0.8.0
|
|
||||||
is-ci: 1.2.1
|
|
||||||
normalize-path: 1.0.0
|
|
||||||
strip-indent: 2.0.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/zod-validation-error@2.1.0(zod@3.23.8):
|
/zod-validation-error@2.1.0(zod@3.23.8):
|
||||||
resolution: {integrity: sha512-VJh93e2wb4c3tWtGgTa0OF/dTt/zoPCPzXq4V11ZjxmEAFaPi/Zss1xIZdEB5RD8GD00U0/iVXgqkF77RV7pdQ==}
|
resolution: {integrity: sha512-VJh93e2wb4c3tWtGgTa0OF/dTt/zoPCPzXq4V11ZjxmEAFaPi/Zss1xIZdEB5RD8GD00U0/iVXgqkF77RV7pdQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|||||||
@@ -226,9 +226,17 @@ export QMSG_TYPE=""
|
|||||||
## ntfy_url 填写ntfy地址,如https://ntfy.sh
|
## ntfy_url 填写ntfy地址,如https://ntfy.sh
|
||||||
## ntfy_topic 填写ntfy的消息应用topic
|
## ntfy_topic 填写ntfy的消息应用topic
|
||||||
## ntfy_priority 填写推送消息优先级,默认为3
|
## ntfy_priority 填写推送消息优先级,默认为3
|
||||||
|
## ntfy_token 填写推送token,可选
|
||||||
|
## ntfy_username 填写推送用户名称,可选
|
||||||
|
## ntfy_password 填写推送用户密码,可选
|
||||||
|
## ntfy_actions 填写推送用户动作,可选
|
||||||
export NTFY_URL=""
|
export NTFY_URL=""
|
||||||
export NTFY_TOPIC=""
|
export NTFY_TOPIC=""
|
||||||
export NTFY_PRIORITY="3"
|
export NTFY_PRIORITY="3"
|
||||||
|
export NTFY_TOKEN=""
|
||||||
|
export NTFY_USERNAME=""
|
||||||
|
export NTFY_PASSWORD=""
|
||||||
|
export NTFY_ACTIONS=""
|
||||||
|
|
||||||
## 21. wxPusher
|
## 21. wxPusher
|
||||||
## 官方文档: https://wxpusher.zjiecode.com/docs/
|
## 官方文档: https://wxpusher.zjiecode.com/docs/
|
||||||
|
|||||||
+92
-34
@@ -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, // 启用一言(随机句子)
|
||||||
|
|
||||||
@@ -17,6 +52,7 @@ const push_config = {
|
|||||||
DD_BOT_TOKEN: '', // 钉钉机器人的 DD_BOT_TOKEN
|
DD_BOT_TOKEN: '', // 钉钉机器人的 DD_BOT_TOKEN
|
||||||
|
|
||||||
FSKEY: '', // 飞书机器人的 FSKEY
|
FSKEY: '', // 飞书机器人的 FSKEY
|
||||||
|
FSSECRET: '', // 飞书机器人的 FSSECRET,对应安全设置里的签名校验密钥
|
||||||
|
|
||||||
// 推送到个人QQ:http://127.0.0.1/send_private_msg
|
// 推送到个人QQ:http://127.0.0.1/send_private_msg
|
||||||
// 群:http://127.0.0.1/send_group_msg
|
// 群:http://127.0.0.1/send_group_msg
|
||||||
@@ -84,7 +120,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 收发件人姓名,可随意填写
|
||||||
|
|
||||||
@@ -104,6 +141,10 @@ const push_config = {
|
|||||||
NTFY_URL: '', // ntfy地址,如https://ntfy.sh,默认为https://ntfy.sh
|
NTFY_URL: '', // ntfy地址,如https://ntfy.sh,默认为https://ntfy.sh
|
||||||
NTFY_TOPIC: '', // ntfy的消息应用topic
|
NTFY_TOPIC: '', // ntfy的消息应用topic
|
||||||
NTFY_PRIORITY: '3', // 推送消息优先级,默认为3
|
NTFY_PRIORITY: '3', // 推送消息优先级,默认为3
|
||||||
|
NTFY_TOKEN: '', // 推送token,可选
|
||||||
|
NTFY_USERNAME: '', // 推送用户名称,可选
|
||||||
|
NTFY_PASSWORD: '', // 推送用户密码,可选
|
||||||
|
NTFY_ACTIONS: '', // 推送用户动作,可选
|
||||||
|
|
||||||
// 官方文档: https://wxpusher.zjiecode.com/docs/
|
// 官方文档: https://wxpusher.zjiecode.com/docs/
|
||||||
// 管理后台: https://wxpusher.zjiecode.com/admin/
|
// 管理后台: https://wxpusher.zjiecode.com/admin/
|
||||||
@@ -122,9 +163,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) {}
|
||||||
@@ -137,9 +178,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) {}
|
||||||
@@ -155,8 +196,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}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,21 +482,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,
|
|
||||||
};
|
|
||||||
options.agent = agent;
|
|
||||||
}
|
}
|
||||||
$.post(options, (err, resp, data) => {
|
$.post(options, (err, resp, data) => {
|
||||||
try {
|
try {
|
||||||
@@ -959,11 +990,26 @@ function aibotkNotify(text, desp) {
|
|||||||
|
|
||||||
function fsBotNotify(text, desp) {
|
function fsBotNotify(text, desp) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const { FSKEY } = push_config;
|
const { FSKEY, FSSECRET } = push_config;
|
||||||
if (FSKEY) {
|
if (FSKEY) {
|
||||||
|
const body = { msg_type: 'text', content: { text: `${text}\n\n${desp}` } };
|
||||||
|
|
||||||
|
// Add signature if secret is provided
|
||||||
|
// Note: Feishu's signature algorithm uses timestamp+"\n"+secret as the HMAC key
|
||||||
|
// and signs an empty message, which differs from typical HMAC usage
|
||||||
|
if (FSSECRET) {
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||||
|
const stringToSign = `${timestamp}\n${FSSECRET}`;
|
||||||
|
const hmac = crypto.createHmac('sha256', stringToSign);
|
||||||
|
const sign = hmac.digest('base64');
|
||||||
|
body.timestamp = timestamp;
|
||||||
|
body.sign = sign;
|
||||||
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
url: `https://open.feishu.cn/open-apis/bot/v2/hook/${FSKEY}`,
|
url: `https://open.feishu.cn/open-apis/bot/v2/hook/${FSKEY}`,
|
||||||
json: { msg_type: 'text', content: { text: `${text}\n\n${desp}` } },
|
json: body,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
@@ -993,7 +1039,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;
|
||||||
}
|
}
|
||||||
@@ -1011,7 +1058,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/>')}`,
|
||||||
});
|
});
|
||||||
@@ -1207,17 +1254,18 @@ function webhookNotify(text, desp) {
|
|||||||
'$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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1230,7 +1278,7 @@ function ntfyNotify(text, desp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const { NTFY_URL, NTFY_TOPIC, NTFY_PRIORITY } = push_config;
|
const { NTFY_URL, NTFY_TOPIC, NTFY_PRIORITY, NTFY_TOKEN, NTFY_USERNAME, NTFY_PASSWORD, NTFY_ACTIONS } = push_config;
|
||||||
if (NTFY_TOPIC) {
|
if (NTFY_TOPIC) {
|
||||||
const options = {
|
const options = {
|
||||||
url: `${NTFY_URL || 'https://ntfy.sh'}/${NTFY_TOPIC}`,
|
url: `${NTFY_URL || 'https://ntfy.sh'}/${NTFY_TOPIC}`,
|
||||||
@@ -1238,9 +1286,19 @@ function ntfyNotify(text, desp) {
|
|||||||
headers: {
|
headers: {
|
||||||
Title: `${encodeRFC2047(text)}`,
|
Title: `${encodeRFC2047(text)}`,
|
||||||
Priority: NTFY_PRIORITY || '3',
|
Priority: NTFY_PRIORITY || '3',
|
||||||
|
Icon: 'https://qn.whyour.cn/logo.png',
|
||||||
},
|
},
|
||||||
timeout,
|
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) => {
|
$.post(options, (err, resp, data) => {
|
||||||
try {
|
try {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
+27
-1
@@ -49,6 +49,7 @@ push_config = {
|
|||||||
'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN
|
'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN
|
||||||
|
|
||||||
'FSKEY': '', # 飞书机器人的 FSKEY
|
'FSKEY': '', # 飞书机器人的 FSKEY
|
||||||
|
'FSSECRET': '', # 飞书机器人的 FSSECRET,对应安全设置里的签名校验密钥
|
||||||
|
|
||||||
'GOBOT_URL': '', # go-cqhttp
|
'GOBOT_URL': '', # go-cqhttp
|
||||||
# 推送到个人QQ:http://127.0.0.1/send_private_msg
|
# 推送到个人QQ:http://127.0.0.1/send_private_msg
|
||||||
@@ -126,6 +127,10 @@ push_config = {
|
|||||||
'NTFY_URL': '', # ntfy地址,如https://ntfy.sh
|
'NTFY_URL': '', # ntfy地址,如https://ntfy.sh
|
||||||
'NTFY_TOPIC': '', # ntfy的消息应用topic
|
'NTFY_TOPIC': '', # ntfy的消息应用topic
|
||||||
'NTFY_PRIORITY':'3', # 推送消息优先级,默认为3
|
'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_APP_TOKEN': '', # wxpusher 的 appToken 官方文档: https://wxpusher.zjiecode.com/docs/ 管理后台: https://wxpusher.zjiecode.com/admin/
|
||||||
'WXPUSHER_TOPIC_IDS': '', # wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
'WXPUSHER_TOPIC_IDS': '', # wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||||
@@ -229,6 +234,20 @@ def feishu_bot(title: str, content: str) -> None:
|
|||||||
|
|
||||||
url = f'https://open.feishu.cn/open-apis/bot/v2/hook/{push_config.get("FSKEY")}'
|
url = f'https://open.feishu.cn/open-apis/bot/v2/hook/{push_config.get("FSKEY")}'
|
||||||
data = {"msg_type": "text", "content": {"text": f"{title}\n\n{content}"}}
|
data = {"msg_type": "text", "content": {"text": f"{title}\n\n{content}"}}
|
||||||
|
|
||||||
|
# Add signature if secret is provided
|
||||||
|
# Note: Feishu's signature algorithm uses timestamp+"\n"+secret as the HMAC key
|
||||||
|
# and signs an empty message, which differs from typical HMAC usage
|
||||||
|
if push_config.get("FSSECRET"):
|
||||||
|
timestamp = str(int(time.time()))
|
||||||
|
string_to_sign = f'{timestamp}\n{push_config.get("FSSECRET")}'
|
||||||
|
hmac_code = hmac.new(
|
||||||
|
string_to_sign.encode("utf-8"), digestmod=hashlib.sha256
|
||||||
|
).digest()
|
||||||
|
sign = base64.b64encode(hmac_code).decode("utf-8")
|
||||||
|
data["timestamp"] = timestamp
|
||||||
|
data["sign"] = sign
|
||||||
|
|
||||||
response = requests.post(url, data=json.dumps(data)).json()
|
response = requests.post(url, data=json.dumps(data)).json()
|
||||||
|
|
||||||
if response.get("StatusCode") == 0 or response.get("code") == 0:
|
if response.get("StatusCode") == 0 or response.get("code") == 0:
|
||||||
@@ -806,7 +825,14 @@ def ntfy(title: str, content: str) -> None:
|
|||||||
encoded_title = encode_rfc2047(title)
|
encoded_title = encode_rfc2047(title)
|
||||||
|
|
||||||
data = content.encode(encoding="utf-8")
|
data = content.encode(encoding="utf-8")
|
||||||
headers = {"Title": encoded_title, "Priority": priority} # 使用编码后的 title
|
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")
|
url = push_config.get("NTFY_URL") + "/" + push_config.get("NTFY_TOPIC")
|
||||||
response = requests.post(url, data=data, headers=headers)
|
response = requests.post(url, data=data, headers=headers)
|
||||||
|
|||||||
@@ -12,4 +12,32 @@ QLAPI.getEnvs({ searchValue: 'dddd' }).then((x) => {
|
|||||||
QLAPI.systemNotify({ title: '123', content: '231' }).then((x) => {
|
QLAPI.systemNotify({ title: '123', content: '231' }).then((x) => {
|
||||||
console.log('systemNotify', x);
|
console.log('systemNotify', x);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 查询定时任务 (Query cron tasks)
|
||||||
|
QLAPI.getCrons({ searchValue: 'test' }).then((x) => {
|
||||||
|
console.log('getCrons', x);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 通过ID查询定时任务 (Get cron by ID)
|
||||||
|
QLAPI.getCronById({ id: 1 }).then((x) => {
|
||||||
|
console.log('getCronById', x);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log('getCronById error', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启用定时任务 (Enable cron tasks)
|
||||||
|
QLAPI.enableCrons({ ids: [1, 2] }).then((x) => {
|
||||||
|
console.log('enableCrons', x);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 禁用定时任务 (Disable cron tasks)
|
||||||
|
QLAPI.disableCrons({ ids: [1, 2] }).then((x) => {
|
||||||
|
console.log('disableCrons', x);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 手动执行定时任务 (Run cron tasks manually)
|
||||||
|
QLAPI.runCrons({ ids: [1] }).then((x) => {
|
||||||
|
console.log('runCrons', x);
|
||||||
|
});
|
||||||
|
|
||||||
console.log('test desc');
|
console.log('test desc');
|
||||||
|
|||||||
+8
-48
@@ -41,14 +41,9 @@ add_cron_api() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:${ql_port}/open/crons?t=$currentTimeStamp" \
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "Authorization: Bearer ${__ql_token__}" \
|
-H "Authorization: Bearer ${__ql_token__}" \
|
||||||
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
|
||||||
-H "Content-Type: application/json;charset=UTF-8" \
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
-H "Origin: http://0.0.0.0:5700" \
|
|
||||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
|
||||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
|
||||||
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
|
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
@@ -76,15 +71,10 @@ update_cron_api() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:${ql_port}/open/crons?t=$currentTimeStamp" \
|
||||||
-X 'PUT' \
|
-X 'PUT' \
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "Authorization: Bearer ${__ql_token__}" \
|
-H "Authorization: Bearer ${__ql_token__}" \
|
||||||
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
|
||||||
-H "Content-Type: application/json;charset=UTF-8" \
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
-H "Origin: http://0.0.0.0:5700" \
|
|
||||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
|
||||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
|
||||||
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
|
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
@@ -108,15 +98,10 @@ update_cron_command_api() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:${ql_port}/open/crons?t=$currentTimeStamp" \
|
||||||
-X 'PUT' \
|
-X 'PUT' \
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "Authorization: Bearer ${__ql_token__}" \
|
-H "Authorization: Bearer ${__ql_token__}" \
|
||||||
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
|
||||||
-H "Content-Type: application/json;charset=UTF-8" \
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
-H "Origin: http://0.0.0.0:5700" \
|
|
||||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
|
||||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
|
||||||
--data-raw "{\"command\":\"${command//\"/\\\"}\",\"id\":\"$id\"}" \
|
--data-raw "{\"command\":\"${command//\"/\\\"}\",\"id\":\"$id\"}" \
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
@@ -133,15 +118,10 @@ del_cron_api() {
|
|||||||
local ids="$1"
|
local ids="$1"
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:${ql_port}/open/crons?t=$currentTimeStamp" \
|
||||||
-X 'DELETE' \
|
-X 'DELETE' \
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "Authorization: Bearer ${__ql_token__}" \
|
-H "Authorization: Bearer ${__ql_token__}" \
|
||||||
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
|
||||||
-H "Content-Type: application/json;charset=UTF-8" \
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
-H "Origin: http://0.0.0.0:5700" \
|
|
||||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
|
||||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
|
||||||
--data-raw "[$ids]" \
|
--data-raw "[$ids]" \
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
@@ -163,15 +143,10 @@ update_cron() {
|
|||||||
local runningTime="${6:-0}"
|
local runningTime="${6:-0}"
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:${ql_port}/open/crons/status?t=$currentTimeStamp" \
|
||||||
-X 'PUT' \
|
-X 'PUT' \
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "Authorization: Bearer ${__ql_token__}" \
|
-H "Authorization: Bearer ${__ql_token__}" \
|
||||||
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
|
||||||
-H "Content-Type: application/json;charset=UTF-8" \
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
-H "Origin: http://0.0.0.0:5700" \
|
|
||||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
|
||||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
|
||||||
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
|
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
@@ -190,15 +165,10 @@ notify_api() {
|
|||||||
local content="$2"
|
local content="$2"
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:${ql_port}/open/system/notify?t=$currentTimeStamp" \
|
||||||
-X 'PUT' \
|
-X 'PUT' \
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "Authorization: Bearer ${__ql_token__}" \
|
-H "Authorization: Bearer ${__ql_token__}" \
|
||||||
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
|
||||||
-H "Content-Type: application/json;charset=UTF-8" \
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
-H "Origin: http://0.0.0.0:5700" \
|
|
||||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
|
||||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
|
||||||
--data-raw "{\"title\":\"${title//\"/\\\"}\",\"content\":\"${content//\"/\\\"}\"}" \
|
--data-raw "{\"title\":\"${title//\"/\\\"}\",\"content\":\"${content//\"/\\\"}\"}" \
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
@@ -215,14 +185,9 @@ find_cron_api() {
|
|||||||
local params="$1"
|
local params="$1"
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/detail?$params&t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:${ql_port}/open/crons/detail?$params&t=$currentTimeStamp" \
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "Authorization: Bearer ${__ql_token__}" \
|
-H "Authorization: Bearer ${__ql_token__}" \
|
||||||
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
|
||||||
-H "Content-Type: application/json;charset=UTF-8" \
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
-H "Origin: http://0.0.0.0:5700" \
|
|
||||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
|
||||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
data=$(echo "$api" | jq -r .data)
|
data=$(echo "$api" | jq -r .data)
|
||||||
@@ -239,15 +204,10 @@ update_auth_config() {
|
|||||||
local tip="$2"
|
local tip="$2"
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/auth/reset?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:${ql_port}/open/system/auth/reset?t=$currentTimeStamp" \
|
||||||
-X 'PUT' \
|
-X 'PUT' \
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "Authorization: Bearer ${__ql_token__}" \
|
-H "Authorization: Bearer ${__ql_token__}" \
|
||||||
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
|
||||||
-H "Content-Type: application/json;charset=UTF-8" \
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
-H "Origin: http://0.0.0.0:5700" \
|
|
||||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
|
||||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
|
||||||
--data-raw "{$body}" \
|
--data-raw "{$body}" \
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
|
|||||||
+5
-24
@@ -20,34 +20,18 @@ copy_dep() {
|
|||||||
echo -e "---> 复制一份 $file_notify_js_sample 为 $file_notify_js\n"
|
echo -e "---> 复制一份 $file_notify_js_sample 为 $file_notify_js\n"
|
||||||
cp -fv $file_notify_js_sample $file_notify_js
|
cp -fv $file_notify_js_sample $file_notify_js
|
||||||
echo -e "---> 通知文件复制完成\n"
|
echo -e "---> 通知文件复制完成\n"
|
||||||
|
|
||||||
echo -e "---> 2. 复制nginx配置文件\n"
|
|
||||||
init_nginx
|
|
||||||
echo -e "---> 配置文件复制完成\n"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pm2_log() {
|
pm2_log() {
|
||||||
echo -e "---> pm2日志"
|
echo -e "---> pm2日志"
|
||||||
local panelOut="/root/.pm2/logs/panel-out.log"
|
local panelOut="/root/.pm2/logs/qinglong-out.log"
|
||||||
local panelError="/root/.pm2/logs/panel-error.log"
|
local panelError="/root/.pm2/logs/qinglong-error.log"
|
||||||
tail -n 300 "$panelOut"
|
tail -n 300 "$panelOut"
|
||||||
tail -n 300 "$panelError"
|
tail -n 300 "$panelError"
|
||||||
}
|
}
|
||||||
|
|
||||||
check_nginx() {
|
|
||||||
local nginxPid=$(ps -eo pid,command | grep nginx | grep -v grep)
|
|
||||||
echo -e "=====> 检测nginx服务\n$nginxPid"
|
|
||||||
if [[ $nginxPid ]]; then
|
|
||||||
echo -e "\n=====> nginx服务正常\n"
|
|
||||||
nginx -s reload
|
|
||||||
else
|
|
||||||
echo -e "\n=====> nginx服务异常,重新启动nginx\n"
|
|
||||||
nginx -c /etc/nginx/nginx.conf
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check_ql() {
|
check_ql() {
|
||||||
local api=$(curl -s --noproxy "*" "http://0.0.0.0:5700")
|
local api=$(curl -s --noproxy "*" "http://0.0.0.0:${ql_port}")
|
||||||
echo -e "\n=====> 检测面板\n\n$api\n"
|
echo -e "\n=====> 检测面板\n\n$api\n"
|
||||||
if [[ $api =~ "<div id=\"root\"></div>" ]]; then
|
if [[ $api =~ "<div id=\"root\"></div>" ]]; then
|
||||||
echo -e "=====> 面板服务启动正常\n"
|
echo -e "=====> 面板服务启动正常\n"
|
||||||
@@ -58,10 +42,10 @@ check_pm2() {
|
|||||||
pm2_log
|
pm2_log
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/system?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:${ql_port}/api/system?t=$currentTimeStamp" \
|
||||||
-H 'Accept: */*' \
|
-H 'Accept: */*' \
|
||||||
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
|
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
|
||||||
-H 'Referer: http://0.0.0.0:5700/crontab' \
|
-H "Referer: http://0.0.0.0:${ql_port}/crontab" \
|
||||||
-H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
|
-H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
@@ -74,14 +58,11 @@ check_pm2() {
|
|||||||
main() {
|
main() {
|
||||||
echo -e "=====> 开始检测"
|
echo -e "=====> 开始检测"
|
||||||
npm i -g pnpm@8.3.1 pm2 ts-node
|
npm i -g pnpm@8.3.1 pm2 ts-node
|
||||||
patch_version
|
|
||||||
|
|
||||||
reset_env
|
reset_env
|
||||||
copy_dep
|
copy_dep
|
||||||
check_ql
|
check_ql
|
||||||
check_nginx
|
|
||||||
check_pm2
|
check_pm2
|
||||||
reload_update
|
|
||||||
reload_pm2
|
reload_pm2
|
||||||
echo -e "\n=====> 检测结束\n"
|
echo -e "\n=====> 检测结束\n"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-15
@@ -5,7 +5,7 @@ const { join } = require('path');
|
|||||||
class GrpcClient {
|
class GrpcClient {
|
||||||
static #config = {
|
static #config = {
|
||||||
protoPath: join(process.env.QL_DIR, 'back/protos/api.proto'),
|
protoPath: join(process.env.QL_DIR, 'back/protos/api.proto'),
|
||||||
serverAddress: '0.0.0.0:5500',
|
serverAddress: `0.0.0.0:${process.env.GRPC_PORT || '5500'}`,
|
||||||
protoOptions: {
|
protoOptions: {
|
||||||
keepCase: true,
|
keepCase: true,
|
||||||
longs: String,
|
longs: String,
|
||||||
@@ -14,9 +14,6 @@ class GrpcClient {
|
|||||||
},
|
},
|
||||||
grpcOptions: {
|
grpcOptions: {
|
||||||
'grpc.enable_http_proxy': 0,
|
'grpc.enable_http_proxy': 0,
|
||||||
'grpc.keepalive_time_ms': 120000,
|
|
||||||
'grpc.keepalive_timeout_ms': 20000,
|
|
||||||
'grpc.max_receive_message_length': 100 * 1024 * 1024,
|
|
||||||
},
|
},
|
||||||
defaultTimeout: 30000,
|
defaultTimeout: 30000,
|
||||||
};
|
};
|
||||||
@@ -36,6 +33,11 @@ class GrpcClient {
|
|||||||
'createCron',
|
'createCron',
|
||||||
'updateCron',
|
'updateCron',
|
||||||
'deleteCrons',
|
'deleteCrons',
|
||||||
|
'getCrons',
|
||||||
|
'getCronById',
|
||||||
|
'enableCrons',
|
||||||
|
'disableCrons',
|
||||||
|
'runCrons',
|
||||||
];
|
];
|
||||||
|
|
||||||
#client;
|
#client;
|
||||||
@@ -59,23 +61,12 @@ class GrpcClient {
|
|||||||
grpc.credentials.createInsecure(),
|
grpc.credentials.createInsecure(),
|
||||||
grpcOptions,
|
grpcOptions,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.#checkConnection();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize gRPC client:', error);
|
console.error('Failed to initialize gRPC client:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#checkConnection() {
|
|
||||||
this.#client.waitForReady(Date.now() + 5000, (error) => {
|
|
||||||
if (error) {
|
|
||||||
console.error('gRPC client connection failed:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#promisifyMethod(methodName) {
|
#promisifyMethod(methodName) {
|
||||||
const capitalizedMethod =
|
const capitalizedMethod =
|
||||||
methodName.charAt(0).toUpperCase() + methodName.slice(1);
|
methodName.charAt(0).toUpperCase() + methodName.slice(1);
|
||||||
|
|||||||
@@ -38,25 +38,48 @@ function run() {
|
|||||||
|
|
||||||
const splitStr = '__sitecustomize__';
|
const splitStr = '__sitecustomize__';
|
||||||
const fileName = process.argv[1].replace(`${dir_scripts}/`, '');
|
const fileName = process.argv[1].replace(`${dir_scripts}/`, '');
|
||||||
let command = `bash -c "source ${file_task_before} ${fileName}`;
|
const tempFile = `/tmp/env_${process.pid}.json`;
|
||||||
|
|
||||||
|
const commands = [
|
||||||
|
`source ${file_task_before} ${fileName}`,
|
||||||
|
task_before ? `eval '${task_before.replace(/'/g, "'\\''")}'` : null,
|
||||||
|
`echo -e '${splitStr}'`,
|
||||||
|
`node -e "require('fs').writeFileSync('${tempFile}', JSON.stringify(process.env))"`,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
if (task_before) {
|
if (task_before) {
|
||||||
const escapeTaskBefore = task_before
|
|
||||||
.replace(/"/g, '\\"')
|
|
||||||
.replace(/\$/g, '\\$');
|
|
||||||
command = `${command} && eval '${escapeTaskBefore}'`;
|
|
||||||
console.log('执行前置命令\n');
|
console.log('执行前置命令\n');
|
||||||
}
|
}
|
||||||
const res = execSync(
|
|
||||||
`${command} && echo -e '${splitStr}' && node -p 'JSON.stringify(process.env)'"`,
|
const res = execSync(commands.join(' && '), {
|
||||||
{
|
encoding: 'utf-8',
|
||||||
encoding: 'utf-8',
|
maxBuffer: 50 * 1024 * 1024,
|
||||||
},
|
shell: '/bin/bash',
|
||||||
);
|
});
|
||||||
const [output, envStr] = res.split(splitStr);
|
|
||||||
const newEnvObject = JSON.parse(envStr.trim());
|
const [output] = res.split(splitStr);
|
||||||
for (const key in newEnvObject) {
|
|
||||||
process.env[key] = newEnvObject[key];
|
try {
|
||||||
|
const envStr = require('fs').readFileSync(tempFile, 'utf-8');
|
||||||
|
const newEnvObject = JSON.parse(envStr);
|
||||||
|
if (typeof newEnvObject === 'object' && newEnvObject !== null) {
|
||||||
|
for (const key in newEnvObject) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(newEnvObject, key)) {
|
||||||
|
process.env[key] = newEnvObject[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require('fs').unlinkSync(tempFile);
|
||||||
|
} catch (jsonError) {
|
||||||
|
console.log(
|
||||||
|
'\ue926 Failed to parse environment variables:',
|
||||||
|
jsonError.message,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
require('fs').unlinkSync(tempFile);
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (output) {
|
if (output) {
|
||||||
console.log(output);
|
console.log(output);
|
||||||
}
|
}
|
||||||
@@ -64,12 +87,10 @@ function run() {
|
|||||||
console.log('执行前置命令结束\n');
|
console.log('执行前置命令结束\n');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!error.message.includes('spawnSync /bin/sh E2BIG')) {
|
if (!error.message.includes('spawnSync /bin/bash E2BIG')) {
|
||||||
console.log(`\ue926 run task before error: `, error);
|
console.log(`\ue926 run task before error: `, error);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
// environment variable is too large
|
||||||
`\ue926 The environment variable is too large. It is recommended to use task_before.js instead of task_before.sh\n`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (task_before) {
|
if (task_before) {
|
||||||
console.log('执行前置命令结束\n');
|
console.log('执行前置命令结束\n');
|
||||||
|
|||||||
@@ -43,47 +43,69 @@ def run():
|
|||||||
|
|
||||||
split_str = "__sitecustomize__"
|
split_str = "__sitecustomize__"
|
||||||
file_name = sys.argv[0].replace(f"{os.getenv('dir_scripts')}/", "")
|
file_name = sys.argv[0].replace(f"{os.getenv('dir_scripts')}/", "")
|
||||||
command = f'bash -c "source {os.getenv("file_task_before")} {file_name}'
|
|
||||||
|
# 创建临时文件路径
|
||||||
|
temp_file = f"/tmp/env_{os.getpid()}.json"
|
||||||
|
|
||||||
|
# 构建命令数组
|
||||||
|
commands = [
|
||||||
|
f'source {os.getenv("file_task_before")} {file_name}'
|
||||||
|
]
|
||||||
|
|
||||||
task_before = os.getenv("task_before")
|
task_before = os.getenv("task_before")
|
||||||
|
|
||||||
if task_before:
|
if task_before:
|
||||||
escape_task_before = task_before.replace('"', '\\"').replace("$", "\\$")
|
escaped_task_before = task_before.replace('"', '\\"').replace("$", "\\$")
|
||||||
command += f" && eval '{escape_task_before}'"
|
commands.append(f"eval '{escaped_task_before}'")
|
||||||
print("执行前置命令\n")
|
print("执行前置命令\n")
|
||||||
|
|
||||||
prev_pythonpath = os.getenv("PREV_PYTHONPATH", "")
|
commands.append(f"echo -e '{split_str}'")
|
||||||
python_command = (
|
|
||||||
"python3 -c 'import os, json; print(json.dumps(dict(os.environ)))'"
|
# 修改 Python 命令,使用单行并正确处理引号
|
||||||
)
|
python_cmd = f"python3 -c 'import os,json; f=open(\\\"{temp_file}\\\",\\\"w\\\"); json.dump(dict(os.environ),f); f.close()'"
|
||||||
command += f" && echo -e '{split_str}' && {python_command}\""
|
commands.append(python_cmd)
|
||||||
|
|
||||||
|
command = " && ".join(cmd for cmd in commands if cmd)
|
||||||
|
command = f'bash -c "{command}"'
|
||||||
|
|
||||||
res = subprocess.check_output(command, shell=True, encoding="utf-8")
|
res = subprocess.check_output(command, shell=True, encoding="utf-8")
|
||||||
output, env_str = res.split(split_str)
|
output = res.split(split_str)[0]
|
||||||
|
|
||||||
env_json = json.loads(env_str.strip())
|
try:
|
||||||
|
with open(temp_file, 'r') as f:
|
||||||
|
env_json = json.loads(f.read())
|
||||||
|
|
||||||
for key, value in env_json.items():
|
for key, value in env_json.items():
|
||||||
os.environ[key] = value
|
os.environ[key] = value
|
||||||
|
|
||||||
|
os.unlink(temp_file)
|
||||||
|
except Exception as json_error:
|
||||||
|
print(f"\ue926 Failed to parse environment variables: {json_error}")
|
||||||
|
try:
|
||||||
|
os.unlink(temp_file)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
if len(output) > 0:
|
if len(output) > 0:
|
||||||
print(output)
|
print(output)
|
||||||
if task_before:
|
if task_before:
|
||||||
print("执行前置命令结束")
|
print("执行前置命令结束\n")
|
||||||
|
|
||||||
except subprocess.CalledProcessError as error:
|
except subprocess.CalledProcessError as error:
|
||||||
print(f"run task before error: {error}")
|
print(f"\ue926 run task before error: {error}")
|
||||||
|
if task_before:
|
||||||
|
print("执行前置命令结束\n")
|
||||||
except OSError as error:
|
except OSError as error:
|
||||||
error_message = str(error)
|
error_message = str(error)
|
||||||
if "Argument list too long" not in error_message:
|
if "Argument list too long" not in error_message:
|
||||||
print(f"\ue926 run task before error: {error}")
|
print(f"\ue926 run task before error: {error}")
|
||||||
else:
|
# else:
|
||||||
print(
|
# environment variable is too large
|
||||||
"\ue926 The environment variable is too large. It is recommended to use task_before.py instead of task_before.sh\n"
|
|
||||||
)
|
|
||||||
if task_before:
|
if task_before:
|
||||||
print("执行前置命令结束")
|
print("执行前置命令结束\n")
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
print(f"run task before error: {error}")
|
print(f"\ue926 run task before error: {error}")
|
||||||
|
if task_before:
|
||||||
|
print("执行前置命令结束\n")
|
||||||
|
|
||||||
import task_before
|
import task_before
|
||||||
|
|
||||||
|
|||||||
+3
-7
@@ -2,13 +2,9 @@
|
|||||||
echo -e "开始发布"
|
echo -e "开始发布"
|
||||||
|
|
||||||
echo -e "切换master分支"
|
echo -e "切换master分支"
|
||||||
git checkout master
|
git branch -D master
|
||||||
|
git checkout -b master
|
||||||
echo -e "合并develop代码"
|
git push --set-upstream origin master -f
|
||||||
git merge origin/develop
|
|
||||||
|
|
||||||
echo -e "提交master代码"
|
|
||||||
git push
|
|
||||||
|
|
||||||
echo -e "更新cdn文件"
|
echo -e "更新cdn文件"
|
||||||
ts-node-transpile-only sample/tool.ts
|
ts-node-transpile-only sample/tool.ts
|
||||||
|
|||||||
+53
-128
@@ -48,8 +48,6 @@ export file_notify_py=$dir_scripts/notify.py
|
|||||||
export file_notify_js=$dir_scripts/sendNotify.js
|
export file_notify_js=$dir_scripts/sendNotify.js
|
||||||
export file_test_js=$dir_scripts/ql_sample.js
|
export file_test_js=$dir_scripts/ql_sample.js
|
||||||
export file_test_py=$dir_scripts/ql_sample.py
|
export file_test_py=$dir_scripts/ql_sample.py
|
||||||
export nginx_app_conf=$dir_root/docker/front.conf
|
|
||||||
export nginx_conf=$dir_root/docker/nginx.conf
|
|
||||||
export dep_notify_py=$dir_dep/notify.py
|
export dep_notify_py=$dir_dep/notify.py
|
||||||
export dep_notify_js=$dir_dep/sendNotify.js
|
export dep_notify_js=$dir_dep/sendNotify.js
|
||||||
|
|
||||||
@@ -61,30 +59,40 @@ list_own_user=$dir_list_tmp/own_user.list
|
|||||||
list_own_add=$dir_list_tmp/own_add.list
|
list_own_add=$dir_list_tmp/own_add.list
|
||||||
list_own_drop=$dir_list_tmp/own_drop.list
|
list_own_drop=$dir_list_tmp/own_drop.list
|
||||||
|
|
||||||
## 软连接及其原始文件对应关系
|
|
||||||
link_name=(
|
link_name=(
|
||||||
task
|
task
|
||||||
ql
|
ql
|
||||||
)
|
)
|
||||||
original_name=(
|
|
||||||
task.sh
|
|
||||||
update.sh
|
|
||||||
)
|
|
||||||
|
|
||||||
init_env() {
|
init_env() {
|
||||||
export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules
|
local pnpm_global_path=$(pnpm root -g 2>/dev/null)
|
||||||
|
export NODE_PATH="/usr/local/bin:/usr/local/lib/node_modules${pnpm_global_path:+:${pnpm_global_path}}"
|
||||||
|
|
||||||
|
# 如果存在 pnpm 全局路径,创建软链接
|
||||||
|
if [[ -n "$pnpm_global_path" ]]; then
|
||||||
|
# 确保目标目录存在
|
||||||
|
mkdir -p "${dir_root}/node_modules"
|
||||||
|
# 链接全局模块到项目的 node_modules
|
||||||
|
ln -sf "${pnpm_global_path}/"* "${dir_root}/node_modules/" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
export PYTHONUNBUFFERED=1
|
export PYTHONUNBUFFERED=1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
load_ql_envs() {
|
||||||
|
ql_base_url=${QlBaseUrl:-"/"}
|
||||||
|
ql_port=${QlPort:-"5700"}
|
||||||
|
ql_grpc_port=${QlGrpcPort:-"5500"}
|
||||||
|
current_branch=${QL_BRANCH:-""}
|
||||||
|
}
|
||||||
|
|
||||||
import_config() {
|
import_config() {
|
||||||
[[ -f $file_config_user ]] && . $file_config_user
|
[[ -f $file_config_user ]] && . $file_config_user
|
||||||
|
|
||||||
ql_base_url=${QlBaseUrl:-"/"}
|
load_ql_envs
|
||||||
ql_port=${QlPort:-"5700"}
|
|
||||||
command_timeout_time=${CommandTimeoutTime:-""}
|
command_timeout_time=${CommandTimeoutTime:-""}
|
||||||
file_extensions=${RepoFileExtensions:-"js py"}
|
file_extensions=${RepoFileExtensions:-"js py"}
|
||||||
proxy_url=${ProxyUrl:-""}
|
proxy_url=${ProxyUrl:-""}
|
||||||
current_branch=${QL_BRANCH:-""}
|
|
||||||
|
|
||||||
if [[ -n "${DefaultCronRule}" ]]; then
|
if [[ -n "${DefaultCronRule}" ]]; then
|
||||||
default_cron="${DefaultCronRule}"
|
default_cron="${DefaultCronRule}"
|
||||||
@@ -170,67 +178,43 @@ fix_config() {
|
|||||||
make_dir $dir_dep
|
make_dir $dir_dep
|
||||||
|
|
||||||
if [[ ! -s $file_config_user ]]; then
|
if [[ ! -s $file_config_user ]]; then
|
||||||
echo -e "复制一份 $file_config_sample 为 $file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
|
cp -f $file_config_sample $file_config_user
|
||||||
cp -fv $file_config_sample $file_config_user
|
|
||||||
echo
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f $file_task_before ]]; then
|
if [[ ! -f $file_task_before ]]; then
|
||||||
echo -e "复制一份 $file_task_sample 为 $file_task_before\n"
|
cp -f $file_task_sample $file_task_before
|
||||||
cp -fv $file_task_sample $file_task_before
|
|
||||||
echo
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f $file_task_after ]]; then
|
if [[ ! -f $file_task_after ]]; then
|
||||||
echo -e "复制一份 $file_task_sample 为 $file_task_after\n"
|
cp -f $file_task_sample $file_task_after
|
||||||
cp -fv $file_task_sample $file_task_after
|
|
||||||
echo
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f $file_extra_shell ]]; then
|
if [[ ! -f $file_extra_shell ]]; then
|
||||||
echo -e "复制一份 $file_extra_sample 为 $file_extra_shell\n"
|
cp -f $file_extra_sample $file_extra_shell
|
||||||
cp -fv $file_extra_sample $file_extra_shell
|
|
||||||
echo
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -s $file_notify_py ]]; then
|
if [[ ! -s $file_notify_py ]]; then
|
||||||
echo -e "复制一份 $file_notify_py_sample 为 $file_notify_py\n"
|
cp -f $file_notify_py_sample $file_notify_py
|
||||||
cp -fv $file_notify_py_sample $file_notify_py
|
|
||||||
echo
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -s $file_notify_js ]]; then
|
if [[ ! -s $file_notify_js ]]; then
|
||||||
echo -e "复制一份 $file_notify_js_sample 为 $file_notify_js\n"
|
cp -f $file_notify_js_sample $file_notify_js
|
||||||
cp -fv $file_notify_js_sample $file_notify_js
|
|
||||||
echo
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -s $file_test_js ]]; then
|
if [[ ! -s $file_test_js ]]; then
|
||||||
cp -fv $file_test_js_sample $file_test_js
|
cp -f $file_test_js_sample $file_test_js
|
||||||
echo
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -s $file_test_py ]]; then
|
if [[ ! -s $file_test_py ]]; then
|
||||||
cp -fv $file_test_py_sample $file_test_py
|
cp -f $file_test_py_sample $file_test_py
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -s /etc/nginx/conf.d/default.conf ]]; then
|
|
||||||
echo -e "检测到默认nginx配置文件,清空...\n"
|
|
||||||
cat /dev/null >/etc/nginx/conf.d/default.conf
|
|
||||||
echo
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -s $dep_notify_js ]]; then
|
if [[ ! -s $dep_notify_js ]]; then
|
||||||
echo -e "复制一份 $file_notify_js_sample 为 $dep_notify_js\n"
|
cp -f $file_notify_js_sample $dep_notify_js
|
||||||
cp -fv $file_notify_js_sample $dep_notify_js
|
|
||||||
echo
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -s $dep_notify_py ]]; then
|
if [[ ! -s $dep_notify_py ]]; then
|
||||||
echo -e "复制一份 $file_notify_py_sample 为 $dep_notify_py\n"
|
cp -f $file_notify_py_sample $dep_notify_py
|
||||||
cp -fv $file_notify_py_sample $dep_notify_py
|
|
||||||
echo
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -288,21 +272,35 @@ random_range() {
|
|||||||
|
|
||||||
delete_pm2() {
|
delete_pm2() {
|
||||||
cd $dir_root
|
cd $dir_root
|
||||||
pm2 delete ecosystem.config.js
|
# Try to delete PM2 processes, but don't fail if PM2 is not available
|
||||||
|
pm2 delete ecosystem.config.js 2>/dev/null || true
|
||||||
|
# Also try to kill any directly spawned node processes
|
||||||
|
pkill -f "node.*static/build/app.js" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
reload_pm2() {
|
reload_pm2() {
|
||||||
cd $dir_root
|
cd $dir_root
|
||||||
restore_env_vars
|
restore_env_vars
|
||||||
pm2 flush &>/dev/null
|
|
||||||
pm2 startOrGracefulReload ecosystem.config.js
|
# Try to start PM2, but handle failures gracefully
|
||||||
}
|
if pm2 flush &>/dev/null && pm2 startOrGracefulReload ecosystem.config.js --update-env; then
|
||||||
|
return 0
|
||||||
reload_update() {
|
else
|
||||||
cd $dir_root
|
local exit_code=$?
|
||||||
restore_env_vars
|
echo "警告: PM2 启动失败 (退出码: $exit_code),可能是由于硬件不兼容"
|
||||||
pm2 flush &>/dev/null
|
echo "正在尝试直接使用 Node.js 启动服务..."
|
||||||
pm2 startOrGracefulReload other.config.js
|
|
||||||
|
# Kill any existing node processes for qinglong
|
||||||
|
pkill -f "node.*static/build/app.js" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Start node directly in the background
|
||||||
|
nohup node static/build/app.js > $dir_log/qinglong.log 2>&1 &
|
||||||
|
local node_pid=$!
|
||||||
|
|
||||||
|
echo "已使用 Node.js 直接启动服务 (PID: $node_pid)"
|
||||||
|
echo "注意: 使用此模式时,部分 PM2 管理功能将不可用"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
diff_time() {
|
diff_time() {
|
||||||
@@ -351,79 +349,6 @@ format_timestamp() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
patch_version() {
|
|
||||||
git config --global pull.rebase false
|
|
||||||
|
|
||||||
if [[ -f "$dir_root/db/cookie.db" ]]; then
|
|
||||||
echo -e "检测到旧的db文件,拷贝为新db...\n"
|
|
||||||
mv $dir_root/db/cookie.db $dir_root/db/env.db
|
|
||||||
rm -rf $dir_root/db/cookie.db
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -d "$dir_root/db" ]]; then
|
|
||||||
echo -e "检测到旧的db目录,拷贝到data目录...\n"
|
|
||||||
cp -rf $dir_root/config $dir_data
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -d "$dir_root/scripts" ]]; then
|
|
||||||
echo -e "检测到旧的scripts目录,拷贝到data目录...\n"
|
|
||||||
cp -rf $dir_root/scripts $dir_data
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -d "$dir_root/log" ]]; then
|
|
||||||
echo -e "检测到旧的log目录,拷贝到data目录...\n"
|
|
||||||
cp -rf $dir_root/log $dir_data
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -d "$dir_root/config" ]]; then
|
|
||||||
echo -e "检测到旧的config目录,拷贝到data目录...\n"
|
|
||||||
cp -rf $dir_root/config $dir_data
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
init_nginx() {
|
|
||||||
cp -fv $nginx_conf /etc/nginx/nginx.conf
|
|
||||||
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
|
|
||||||
local location_url="/"
|
|
||||||
local aliasStr=""
|
|
||||||
local rootStr=""
|
|
||||||
if [[ $ql_base_url != "/" ]]; then
|
|
||||||
if [[ $ql_base_url != /* ]]; then
|
|
||||||
ql_base_url="/$ql_base_url"
|
|
||||||
fi
|
|
||||||
if [[ $ql_base_url != */ ]]; then
|
|
||||||
ql_base_url="$ql_base_url/"
|
|
||||||
fi
|
|
||||||
location_url="^~${ql_base_url%*/}"
|
|
||||||
aliasStr="alias ${dir_static}/dist;"
|
|
||||||
if ! grep -q "<base href=\"$ql_base_url\">" "${dir_static}/dist/index.html"; then
|
|
||||||
awk -v text="<base href=\"$ql_base_url\">" '/<link/ && !inserted {print text; inserted=1} 1' "${dir_static}/dist/index.html" >temp.html
|
|
||||||
mv temp.html "${dir_static}/dist/index.html"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
rootStr="root ${dir_static}/dist;"
|
|
||||||
fi
|
|
||||||
sed -i "s,QL_ALIAS_CONFIG,${aliasStr},g" /etc/nginx/conf.d/front.conf
|
|
||||||
sed -i "s,QL_ROOT_CONFIG,${rootStr},g" /etc/nginx/conf.d/front.conf
|
|
||||||
sed -i "s,QL_BASE_URL_LOCATION,${location_url},g" /etc/nginx/conf.d/front.conf
|
|
||||||
sed -i "s,QL_BASE_URL,${ql_base_url},g" /etc/nginx/conf.d/front.conf
|
|
||||||
|
|
||||||
local ipv6=$(ip a | grep inet6)
|
|
||||||
local ipv6Str=""
|
|
||||||
if [[ $ipv6 ]]; then
|
|
||||||
ipv6Str="listen [::]:${ql_port} ipv6only=on;"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local ipv4Str="listen ${ql_port};"
|
|
||||||
sed -i "s,IPV6_CONFIG,${ipv6Str},g" /etc/nginx/conf.d/front.conf
|
|
||||||
sed -i "s,IPV4_CONFIG,${ipv4Str},g" /etc/nginx/conf.d/front.conf
|
|
||||||
}
|
|
||||||
|
|
||||||
get_env_array() {
|
get_env_array() {
|
||||||
exported_variables=()
|
exported_variables=()
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
|
|||||||
+19
-10
@@ -46,18 +46,22 @@ handle_log_path() {
|
|||||||
|
|
||||||
time=$(date "+$mtime_format")
|
time=$(date "+$mtime_format")
|
||||||
log_time=$(format_log_time "$mtime_format" "$time")
|
log_time=$(format_log_time "$mtime_format" "$time")
|
||||||
log_dir_tmp="${file_param##*/}"
|
if [[ -z $log_name ]]; then
|
||||||
if [[ $file_param =~ "/" ]]; then
|
log_dir_tmp="${file_param##*/}"
|
||||||
if [[ $file_param == /* ]]; then
|
if [[ $file_param =~ "/" ]]; then
|
||||||
log_dir_tmp_path="${file_param:1}"
|
if [[ $file_param == /* ]]; then
|
||||||
else
|
log_dir_tmp_path="${file_param:1}"
|
||||||
log_dir_tmp_path="${file_param}"
|
else
|
||||||
|
log_dir_tmp_path="${file_param}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
||||||
|
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||||
|
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||||
|
log_dir="${log_dir_tmp%.*}${suffix}"
|
||||||
|
else
|
||||||
|
log_dir="$log_name"
|
||||||
fi
|
fi
|
||||||
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
|
||||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
|
||||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
|
||||||
log_dir="${log_dir_tmp%.*}${suffix}"
|
|
||||||
log_path="$log_dir/$log_time.log"
|
log_path="$log_dir/$log_time.log"
|
||||||
|
|
||||||
if [[ ${real_log_path:=} ]]; then
|
if [[ ${real_log_path:=} ]]; then
|
||||||
@@ -73,6 +77,11 @@ handle_log_path() {
|
|||||||
if [[ "${real_time:=}" == "true" ]]; then
|
if [[ "${real_time:=}" == "true" ]]; then
|
||||||
cmd=""
|
cmd=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${log_dir:=}" == "/dev/null" ]]; then
|
||||||
|
cmd=">> /dev/null"
|
||||||
|
log_path="/dev/null"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
format_params() {
|
format_params() {
|
||||||
|
|||||||
+22
-20
@@ -3,6 +3,7 @@
|
|||||||
dir_shell=$QL_DIR/shell
|
dir_shell=$QL_DIR/shell
|
||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
. $dir_shell/api.sh
|
. $dir_shell/api.sh
|
||||||
|
load_ql_envs
|
||||||
. $dir_shell/env.sh
|
. $dir_shell/env.sh
|
||||||
|
|
||||||
send_mark=$dir_shell/send_mark
|
send_mark=$dir_shell/send_mark
|
||||||
@@ -212,20 +213,25 @@ run_extra_shell() {
|
|||||||
|
|
||||||
## 脚本用法
|
## 脚本用法
|
||||||
usage() {
|
usage() {
|
||||||
echo -e "ql命令使用方法:"
|
echo -e "$cmd_update 命令使用方法:"
|
||||||
echo -e "1. $cmd_update update # 更新并重启青龙"
|
echo -e "1. $cmd_update update # 更新并重启青龙"
|
||||||
echo -e "2. $cmd_update extra # 运行自定义脚本"
|
echo -e "2. $cmd_update extra # 运行自定义脚本"
|
||||||
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
|
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
|
||||||
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> <extensions> # 更新单个仓库的脚本"
|
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> <extensions> # 更新单个仓库的脚本"
|
||||||
echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
|
echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
|
||||||
echo -e "6. $cmd_update bot # 启动tg-bot"
|
echo -e "6. $cmd_update bot # 启动tg-bot"
|
||||||
echo -e "7. $cmd_update check # 检测青龙环境并修复"
|
echo -e "7. $cmd_update check # 检测青龙环境并修复"
|
||||||
echo -e "8. $cmd_update resetlet # 重置登录错误次数"
|
echo -e "8. $cmd_update resetlet # 重置登录错误次数"
|
||||||
echo -e "9. $cmd_update resettfa # 禁用两步登录"
|
echo -e "9. $cmd_update resettfa # 禁用两步登录"
|
||||||
|
echo -e "10. $cmd_update resetpwd # 修改登录密码"
|
||||||
|
echo -e "11. $cmd_update resetname # 修改登录用户名"
|
||||||
}
|
}
|
||||||
|
|
||||||
reload_qinglong() {
|
reload_qinglong() {
|
||||||
|
echo -e "[reload_qinglong] deleting Triggered at $(date)" >>${dir_log}/reload.log
|
||||||
|
sleep 3
|
||||||
delete_pm2
|
delete_pm2
|
||||||
|
echo -e "[reload_qinglong] deleted Triggered at $(date)" >>${dir_log}/reload.log
|
||||||
|
|
||||||
local reload_target="${1}"
|
local reload_target="${1}"
|
||||||
local primary_branch="master"
|
local primary_branch="master"
|
||||||
@@ -245,8 +251,9 @@ reload_qinglong() {
|
|||||||
rm -rf ${dir_data}/*
|
rm -rf ${dir_data}/*
|
||||||
mv -f ${dir_tmp}/data/* ${dir_data}/
|
mv -f ${dir_tmp}/data/* ${dir_data}/
|
||||||
fi
|
fi
|
||||||
|
echo -e "[reload_qinglong] starting Triggered at $(date)" >>${dir_log}/reload.log
|
||||||
reload_pm2
|
reload_pm2
|
||||||
|
echo -e "[reload_qinglong] started Triggered at $(date)\n" >>${dir_log}/reload.log
|
||||||
}
|
}
|
||||||
|
|
||||||
## 更新 qinglong
|
## 更新 qinglong
|
||||||
@@ -307,15 +314,7 @@ check_update_dep() {
|
|||||||
echo -e "更新包下载成功..."
|
echo -e "更新包下载成功..."
|
||||||
|
|
||||||
if [[ "$needRestart" == 'true' ]]; then
|
if [[ "$needRestart" == 'true' ]]; then
|
||||||
delete_pm2
|
reload_qinglong "system"
|
||||||
|
|
||||||
rm -rf ${dir_root}/back ${dir_root}/cli ${dir_root}/docker ${dir_root}/sample ${dir_root}/shell ${dir_root}/src
|
|
||||||
mv -f ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/
|
|
||||||
rm -rf $dir_static/*
|
|
||||||
mv -f ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/
|
|
||||||
cp -f $file_config_sample $dir_config/config.sample.sh
|
|
||||||
|
|
||||||
reload_pm2
|
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo -e "\n依赖检测安装失败,请检查网络...\n"
|
echo -e "\n依赖检测安装失败,请检查网络...\n"
|
||||||
@@ -546,6 +545,9 @@ main() {
|
|||||||
resetpwd)
|
resetpwd)
|
||||||
eval update_auth_config "\\\"password\\\":\\\"$p2\\\"" "重置密码" $cmd
|
eval update_auth_config "\\\"password\\\":\\\"$p2\\\"" "重置密码" $cmd
|
||||||
;;
|
;;
|
||||||
|
resetname)
|
||||||
|
eval update_auth_config "\\\"username\\\":\\\"$p2\\\"" "重置用户名" $cmd
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
eval echo -e "命令输入错误...\\\n" $cmd
|
eval echo -e "命令输入错误...\\\n" $cmd
|
||||||
eval usage $cmd
|
eval usage $cmd
|
||||||
|
|||||||
+12
-5
@@ -7,11 +7,18 @@ export function rootContainer(container: any) {
|
|||||||
'en': require('./locales/en-US.json'),
|
'en': require('./locales/en-US.json'),
|
||||||
'zh': require('./locales/zh-CN.json'),
|
'zh': require('./locales/zh-CN.json'),
|
||||||
};
|
};
|
||||||
let currentLocale = intl.determineLocale({
|
let currentLocale: string;
|
||||||
urlLocaleKey: 'lang',
|
try {
|
||||||
cookieLocaleKey: 'lang',
|
currentLocale = intl.determineLocale({
|
||||||
localStorageLocaleKey: 'lang',
|
urlLocaleKey: 'lang',
|
||||||
}).slice(0, 2);
|
cookieLocaleKey: 'lang',
|
||||||
|
localStorageLocaleKey: 'lang',
|
||||||
|
}).slice(0, 2);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
// Handle decodeURIComponent errors from malformed cookies
|
||||||
|
console.warn('Failed to determine locale from cookies:', e);
|
||||||
|
currentLocale = '';
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentLocale || !Object.keys(locales).includes(currentLocale)) {
|
if (!currentLocale || !Object.keys(locales).includes(currentLocale)) {
|
||||||
currentLocale = 'zh';
|
currentLocale = 'zh';
|
||||||
|
|||||||
+16
-11
@@ -1,6 +1,6 @@
|
|||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import React, { useRef, useState, useEffect } from 'react';
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
import { Tooltip, Typography } from 'antd';
|
import { Tooltip, Typography, message } from 'antd';
|
||||||
import { CopyOutlined, CheckOutlined } from '@ant-design/icons';
|
import { CopyOutlined, CheckOutlined } from '@ant-design/icons';
|
||||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||||
|
|
||||||
@@ -10,16 +10,21 @@ const Copy = ({ text }: { text: string }) => {
|
|||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const copyIdRef = useRef<number>();
|
const copyIdRef = useRef<number>();
|
||||||
|
|
||||||
const copyText = (e?: React.MouseEvent) => {
|
const handleCopy = (text: string, result: boolean) => {
|
||||||
|
if (result) {
|
||||||
|
setCopied(true);
|
||||||
|
message.success(intl.get('复制成功'));
|
||||||
|
|
||||||
|
cleanCopyId();
|
||||||
|
copyIdRef.current = window.setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = (e?: React.MouseEvent) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
|
|
||||||
setCopied(true);
|
|
||||||
|
|
||||||
cleanCopyId();
|
|
||||||
copyIdRef.current = window.setTimeout(() => {
|
|
||||||
setCopied(false);
|
|
||||||
}, 3000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanCopyId = () => {
|
const cleanCopyId = () => {
|
||||||
@@ -27,8 +32,8 @@ const Copy = ({ text }: { text: string }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link onClick={copyText} style={{ marginLeft: 1 }}>
|
<Link onClick={handleClick} style={{ marginLeft: 4 }}>
|
||||||
<CopyToClipboard text={text}>
|
<CopyToClipboard text={text} onCopy={handleCopy}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key="copy"
|
key="copy"
|
||||||
title={copied ? intl.get('复制成功') : intl.get('复制')}
|
title={copied ? intl.get('复制成功') : intl.get('复制')}
|
||||||
|
|||||||
@@ -101,16 +101,21 @@ export default function () {
|
|||||||
|
|
||||||
const getHealthStatus = () => {
|
const getHealthStatus = () => {
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}public/health`)
|
.get(`${config.apiPrefix}health`)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res?.data?.status === 1) {
|
if (res?.data?.status === 'ok') {
|
||||||
getSystemInfo();
|
getSystemInfo();
|
||||||
} else {
|
} else {
|
||||||
history.push('/error');
|
history.push('/error');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
history.push('/error');
|
const responseStatus = error.response.status;
|
||||||
|
if (responseStatus !== 401) {
|
||||||
|
history.push('/error');
|
||||||
|
} else {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setInitLoading(false));
|
.finally(() => setInitLoading(false));
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user