Compare commits

...
Author SHA1 Message Date
copilot-swe-agent[bot]andwhyour f33d5e8cdf Fix PM2 log file path and add existence checks
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-11-08 17:21:08 +00:00
copilot-swe-agent[bot] 7d2a570924 Initial plan 2025-11-08 17:15:12 +00:00
whyour 73f8f3c5fa 增加 agent
Create a new agent configuration file for QL agent.
2025-11-08 00:05:56 +08:00
whyour 494ccb3c08 更新 workflow 2025-11-03 00:09:36 +08:00
whyour 399728b433 修复 jwt 认证 2025-11-02 22:28:58 +08:00
whyour 52a1de5063 更新 demo 地址 2025-11-02 21:54:19 +08:00
whyour 18f27a9a69 移除 nginx 2025-11-02 19:29:59 +08:00
whyour 07951964a1 修复模块注入 2025-10-26 22:32:03 +08:00
涛之雨andGitHub a1f888af59 Add validation to dependencies GET endpoint and update service logic (#2778)
* Add validation to dependencies GET endpoint and update service logic

* fix https://github.com/whyour/qinglong/pull/2778/files/6063bc3a67fb329de9b90f7c93524b862bd9eb93#r2266494581

* remove default condition type

* fix query mistakes
2025-10-11 23:23:13 +08:00
涛之雨andGitHub f7472b6e74 Add input validation to script API routes (#2777)
* Add input validation to script API routes

* 优化脚本 API 路由的错误处理逻辑

* Fix optional path compatibility checks

* remove file
2025-10-11 23:20:26 +08:00
whyour a7baeba755 修复 task 命令可能软链失败 2025-10-11 23:15:54 +08:00
涛之雨andGitHub e4f733320d Enable debug backend (#2776) 2025-08-19 11:13:33 +08:00
whyour 55c92dc320 发布版本 v2.19.2 2025-07-12 20:29:49 +08:00
whyour 50769c43dd 修复 command-run 接口日志绑定 2025-07-12 20:29:44 +08:00
whyour 0587644a6b command-run 增加返回 QL-Task-Log 日志路径 2025-06-28 01:05:05 +08:00
whyour 87b934aafe QLAPI.systemNotify 支持自定义通知类型和参数 2025-06-24 02:00:51 +08:00
whyour 7a92e7c6ab 修复取消安装依赖 2025-06-22 21:47:39 +08:00
whyour 1d8403c0ec 修复环境变量过大解析 2025-06-22 21:10:55 +08:00
whyour ef9e38f167 备份数据支持选择模块,支持清除依赖缓存 2025-06-22 14:25:19 +08:00
whyour c9bd053fbd 修改服务启动方式 2025-06-11 00:42:29 +08:00
憶夣andGitHub 57939391b9 ntfy 增加可选的认证与用户动作 (#2741)
* feat:ntfy增加可选的认证

* feat:ntfy增加可选的用户动作

* fix:ntfy动作包含中文报错
2025-06-07 00:26:27 +08:00
whyour 394e96bbf8 修复 health 接口报错 2025-06-07 00:25:47 +08:00
whyour 47c194c1f4 更新版本 v2.19.1 2025-05-24 15:03:08 +08:00
whyour 7d65d96ebd 修复 demo 环境提示 2025-05-24 14:56:49 +08:00
whyour 224000b63b 修复依赖是否安装检查逻辑 2025-05-23 23:45:43 +08:00
whyour 1c18668bad 修复文件下载参数 2025-05-22 00:09:19 +08:00
whyour f94582b68d 修复查询 python 依赖存在逻辑 2025-05-21 01:25:24 +08:00
whyour eb1c00984c 修复任务视图状态包含筛选 2025-05-20 23:40:18 +08:00
whyour 1a185f5682 修复创建脚本可能失败 2025-05-20 01:00:08 +08:00
whyour b6ea8565ec 更新版本 v2.19.0 2025-05-18 22:02:57 +08:00
whyour 2e94d58758 修改启动命令环境变量 2025-05-18 22:02:55 +08:00
whyour 472a3088df 修复启动逻辑 2025-05-17 17:25:50 +08:00
whyour ec3d61a713 修复 /:file 获取日志接口 2025-05-16 00:57:27 +08:00
whyour 95459c33ed 修改 import undici 2025-05-15 01:19:24 +08:00
whyour 5459b72f63 更新 pnpm-lock 2025-05-15 01:15:55 +08:00
whyour 05db2b1df8 修复重置用户名失败 2025-05-15 01:12:01 +08:00
whyour c3072e7712 got 替换为 uudici 2025-05-15 01:01:39 +08:00
whyour 3fafe4d24d 修复依赖强制删除未移除队列 2025-05-13 02:00:37 +08:00
whyour 32bccb3f3e 修复无法识别 python 依赖安装的命令 2025-05-13 00:22:15 +08:00
whyour ac04478d1d 修复重用户名 2025-05-11 18:59:15 +08:00
whyour 8a18baa921 修复登录通知失败造成服务重启 2025-05-11 14:36:16 +08:00
whyour 9a399f8de8 更新 workflow 2025-05-09 23:36:14 +08:00
whyour da639a1f8f 移除部分历史兼容逻辑 2025-05-09 15:37:05 +08:00
whyour 425e49675a 修复删除日志 2025-05-09 00:20:53 +08:00
whyour 8174762c18 demo 环境不自动运行任务 2025-05-08 01:43:14 +08:00
whyour 710a107e73 邮箱通知支持多个收件人 2025-05-08 01:27:25 +08:00
whyour d871585eee 修改服务启动逻辑 2025-05-07 09:30:00 +08:00
whyour 729b405b0f 修复 bootAfter 目录 2025-04-26 23:26:11 +08:00
whyour 71a7c1b9d3 boot 任务改为在依赖安装完成后执行 2025-04-25 23:52:33 +08:00
whyour 40a831f3a2 修复脚本管理查询逻辑 2025-04-25 01:40:37 +08:00
whyour 1befa1bb8c 缓存 node 和 python 依赖 2025-04-23 02:10:39 +08:00
whyour 8009634b44 修复脚本管理增加文件夹 2025-04-19 16:34:28 +08:00
whyour 03a23f6f31 更新 workflow 运行环境 2025-04-19 14:36:21 +08:00
whyour 124e01e93a 修复 QLAPI 修复环境变量 remarks,esm 依赖查不到 2025-04-19 01:38:43 +08:00
whyour 2370923a75 更新版本 v2.18.3 2025-03-19 23:47:53 +08:00
whyour 256e4e7a83 修复任务打开脚本链接 2025-03-19 23:47:51 +08:00
whyour c0ec063333 修复删除脚本参数 2025-03-18 23:17:03 +08:00
whyour 576408de01 脚本管理增加强制打开文件操作 2025-03-16 17:57:13 +08:00
whyour cf94ecfb11 脚本管理和日志管理支持下载 2025-03-13 00:22:24 +08:00
whyour 118c92d9e5 更新版本 v2.18.2 2025-02-28 00:56:43 +08:00
whyour 280085668e 修复未初始化时区设置 2025-02-28 00:56:40 +08:00
whyour bae4073a64 增加重置密码命令 2025-02-27 23:57:26 +08:00
whyour af3e358a6a 系统设置增加时区设置 2025-02-27 00:45:21 +08:00
whyour 64fcbff715 脚本管理增加可预览检查 2025-02-26 01:17:11 +08:00
whyour f9f78b4e05 修改系统内置通知模块名称,避免重复 2025-02-25 00:32:13 +08:00
whyour fa83761d27 修改定时规则类型 2025-02-21 01:35:08 +08:00
whyour 8173075b67 定时任务支持 @once 和 @boot 任务 2025-02-20 02:18:59 +08:00
whyour 496918131f 修复群晖通知参数,任务视图不属于筛选 2025-02-16 12:15:45 +08:00
whyour 5d64aae452 修复 mirror action 2025-02-07 01:56:23 +08:00
whyour 952ea2859e 修复最后运行时长多语言显示 2025-01-31 23:02:25 +08:00
whyour 25e8e8198d 修改 QLAPI 调用校验 2025-01-30 00:55:48 +08:00
whyour 3ab5b0d86b 修复登录失败没有提示 2025-01-29 23:50:46 +08:00
whyour 6df651aa63 更新 ts-proto 版本 2025-01-29 23:29:45 +08:00
whyour 924dfd6a6f 更新版本 v2.18.1 2025-01-22 01:17:15 +08:00
whyour 07f43538df 修复写入文件 mode 2025-01-22 01:17:13 +08:00
whyour 4fa5fa2014 修改 lock 文件名称规则 2025-01-16 00:35:47 +08:00
whyour f9b9543a6d 修复 JavaScript telegram 通知 2025-01-15 23:23:54 +08:00
whyour af97543918 修改错误提示 2025-01-14 23:20:53 +08:00
whyour a508522872 增加安全报告提示 2025-01-14 01:05:48 +08:00
whyour f1ca2134b7 移除 nedb 和 sentry 2025-01-14 00:24:25 +08:00
whyour a9755655b2 更新 readme 2025-01-12 18:32:48 +08:00
whyour 6a76e82f26 修改 QLAPI python 示例 2025-01-12 18:11:42 +08:00
whyour 6775f5d123 修复 QLAPI 系统通知响应 2025-01-12 17:02:21 +08:00
whyour ad6e08525c 修改 QLAPI 系统通知 2025-01-12 15:39:28 +08:00
whyour 51ef4e7476 修改任务状态更新失败提示,重复运行提示 2025-01-12 00:19:14 +08:00
whyour e5b35273f9 修复更新环境变量 2025-01-11 17:14:30 +08:00
whyour 647ed3b66c QLAPI 支持操作环境变量和系统通知 2025-01-11 01:59:46 +08:00
143 changed files with 10077 additions and 4304 deletions
+8 -11
View File
@@ -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=''
+4
View File
@@ -0,0 +1,4 @@
---
name: Bug Fixer
description: Fix this issue following our error handling pattern.
---
@@ -14,27 +14,35 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
to_gitlab: code_gitlab:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1 - uses: Yikun/hub-mirror-action@master
with: with:
target_repo_url: git@gitlab.com:whyour/qinglong.git src: github/whyour
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }} dst: gitlab/whyour
dst_key: ${{ secrets.GITLAB_SSH_PK }}
dst_token: ${{ secrets.GITLAB_TOKEN }}
static_list: "qinglong"
force_update: true
to_gitee: code_gitee:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1 - uses: Yikun/hub-mirror-action@master
with: with:
target_repo_url: git@gitee.com:whyour/qinglong.git src: github/whyour
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }} dst: gitee/whyour
dst_key: ${{ secrets.GITLAB_SSH_PK }}
dst_token: ${{ secrets.GITEE_TOKEN }}
static_list: "qinglong"
force_update: true
build-static: build-static:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -71,23 +79,42 @@ jobs:
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')" git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH} git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
mkdir -p ~/.ssh static_gitlab:
echo "${PRIVATE_KEY}" > ~/.ssh/id_rsa needs: build-static
chmod 600 ~/.ssh/id_rsa runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: Yikun/hub-mirror-action@master
with:
src: github/whyour
dst: gitlab/whyour
dst_key: ${{ secrets.GITLAB_SSH_PK }}
dst_token: ${{ secrets.GITLAB_TOKEN }}
static_list: "qinglong-static"
force_update: true
export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l git" static_gitee:
git remote add gitee "${REPO_GITEE}" needs: build-static
git remote add gitlab "${REPO_GITLAB}" runs-on: ubuntu-latest
git gc steps:
git push --force --quiet gitee ${GITHUB_BRANCH}:${GITHUB_BRANCH} - uses: actions/checkout@v4
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH} with:
fetch-depth: 0
- uses: Yikun/hub-mirror-action@master
with:
src: github/whyour
dst: gitee/whyour
dst_key: ${{ secrets.GITLAB_SSH_PK }}
dst_token: ${{ secrets.GITEE_TOKEN }}
static_list: "qinglong-static"
force_update: true
build: build:
needs: build-static needs: build-static
# 由于 ubuntu-latest 中使用 linux6.x 内核 linux/s390x npm 无法使用 runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
# runs-on: self-hosted
permissions: permissions:
packages: write packages: write
@@ -156,8 +183,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
@@ -174,7 +201,7 @@ jobs:
if: ${{ github.ref_name == 'master' }} if: ${{ github.ref_name == 'master' }}
needs: build-static needs: build-static
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
permissions: permissions:
packages: write packages: write
@@ -222,7 +249,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/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
+1
View File
@@ -27,3 +27,4 @@ __pycache__
/shell/preload/env.* /shell/preload/env.*
/shell/preload/notify.* /shell/preload/notify.*
/shell/preload/*-notify.json /shell/preload/*-notify.json
/shell/preload/__ql_notify__.*
+1 -11
View File
@@ -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' },
+6 -129
View File
@@ -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>
![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png) ![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
@@ -56,138 +56,15 @@ npm i @whyour/qinglong
## Deployment ## Deployment
### Docker (Recommended) [View Documentation](https://qinglong.online/guide/getting-started/installation-guide)
```bash ## Built-in API
# curl -sSL get.docker.com | sh
docker run -dit \
-v $PWD/ql/data:/ql/data \
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
-p 5700:5700 \
# Deployment paths are not required, e.g. /test.
-e QlBaseUrl="/" \
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
-e QlPort="5700" \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
whyour/qinglong:latest
```
### BaoTa Panel one-click deployment (Recommended) [View Documentation](https://qinglong.online/guide/user-guide/built-in-api)
1. To install Pagoda Panel, go to the official website of [BaoTa Panel](https://www.bt.cn/u/EcDAFU), select the official version of the script to download and install.
2. After installation, login to Pagoda Panel, click `Docker` in the menu bar, the first time you enter, you will be prompted to install `Docker` service, click Install Now, follow the prompts to complete the installation.
3. After the installation is complete, find `Qinglong Panel` in the app shop, click Install, configure the domain name and other basic information to complete the installation.
### Docker-compose (Recommended)
```bash
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
# start
docker-compose up -d
# stop
docker-compose down
```
### Podman (Recommended)
```bash
# https://podman.io/getting-started/installation
podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
-p 5700:5700 \
# Deployment paths are not required, e.g. /test.
-e QlBaseUrl="/" \
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
-e QlPort="5700" \
--name qinglong \
--hostname qinglong \
docker.io/whyour/qinglong:latest
```
### Npm (Local)
It is recommended to use a pure system installation to avoid losing the original system data, you need to install node/npm/python3/pip3/pnpm yourself
```bash
# Debian/Ubuntu
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
```
```bash
npm install -g node-pre-gyp pnpm@8.3.1
npm install -g @whyour/qinglong
qinglong
# Add the environment variables QL_DIR and QL_DATA_DIR when prompted, QL_DATA_DIR must end with /data.
export QL_DIR=""
export QL_DATA_DIR=""
# Run again
qinglong
```
## Built-in commands ## Built-in commands
- task [View Documentation](https://qinglong.online/guide/user-guide/basic-explanation)
```bash
# Execute in sequence, if a random delay is set, it will be randomly delayed by a certain number of seconds
task <file_path>
# Execute in sequence, regardless of whether a random delay is set, all run immediately,
# and the foreground will output the day, while recorded in the log file
task <file_path> now
# Concurrent execution, regardless of whether a random delay is set, are run immediately,
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
task <file_path> conc <env_name> <account_number>(Optional)
# Specify the account to execute and run immediately regardless of whether a random delay is set
task <file_path> desi <env_name> <account_number>
# Set task timeout
task -m <max_time> <file_path>
# Use -- to split, -- followed by a parameter that is passed to the script, as in the following example, the script receives the parameter -u whyour -p password
task <file_path> -- -u whyour -p password
```
- ql
```bash
# Update and restart Green Dragon
ql update
# Run custom scripts extra.sh
ql extra
# Adding a single script file
ql raw <file_url>
# Add a specific script for a single repository
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
# Delete old logs
ql rmlog <days>
# Start bot
ql bot
# Detecting the Green Dragon environment and repairing it
ql check
# Reset the number of login errors
ql resetlet
# Disable two-step login
ql resettfa
```
| **Parameter** | **Description** |
|---|---|
| file_url | Script address |
| repo_url | Repository address |
| whitelist | The whitelist when pulling the repository, i.e., the string contained in the path of the script to be pulled |
| blacklist | Blacklisting when pulling repositories, i.e. strings that are not included in the path of the script to be pulled |
| dependence | Pulling the dependencies needed for the repository will be copied directly from the repository to the repository directory under scripts, regardless of the blacklist |
| extensions | Pull the branch of the repository |
| branch | Number of days of logs to be kept |
| days | File path for task execution |
| file_path | The name of the environment variable that needs to be concurrent or specified at the time of task execution |
## Development ## Development
+6 -127
View File
@@ -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>
![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png) ![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
@@ -58,136 +58,15 @@ npm i @whyour/qinglong
## 部署 ## 部署
### docker (推荐) [查看文档](https://qinglong.online/guide/getting-started/installation-guide)
```bash ## 内置 API
# curl -sSL get.docker.com | sh
docker run -dit \
-v $PWD/ql/data:/ql/data \
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
-p 5700:5700 \
# 部署路径非必须,比如 /test
-e QlBaseUrl="/" \
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
-e QlPort="5700" \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
whyour/qinglong:latest
```
### 宝塔面板一键部署(推荐) [查看文档](https://qinglong.online/guide/user-guide/built-in-api)
1. 安装宝塔面板,前往 [宝塔面板](https://www.bt.cn/u/EcDAFU) 官网,选择正式版的脚本下载安装
2. 安装后登录宝塔面板,在菜单栏中点击 `Docker`,首次进入会提示安装`Docker`服务,点击立即安装,按提示完成安装
3. 安装完成后在应用商店中找到`青龙面板`,点击安装,配置域名等基本信息即可完成安装
### docker-compose (推荐)
```bash
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
mkdir qinglong && cd $_
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
# 启动
docker-compose up -d
# 停止
docker-compose down
```
### podman (推荐)
```bash
# https://podman.io/getting-started/installation
podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
-p 5700:5700 \
# 部署路径非必须,比如 /test
-e QlBaseUrl="/" \
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
-e QlPort="5700" \
--name qinglong \
--hostname qinglong \
docker.io/whyour/qinglong:latest
```
### npm (本地)
建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3/pnpm
```bash
# Debian/Ubuntu
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
```
```bash
npm install -g node-pre-gyp pnpm@8.3.1
npm install -g @whyour/qinglong
qinglong
# 根据提示增加环境变量 QL_DIR 和 QL_DATA_DIRQL_DATA_DIR 必须以 /data 结尾
export QL_DIR=""
export QL_DATA_DIR=""
# 再次执行
qinglong
```
## 内置命令 ## 内置命令
- task [查看文档](https://qinglong.online/guide/user-guide/basic-explanation)
```bash
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
task <file_path>
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
task <file_path> now
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
task <file_path> conc <env_name> <account_number>(可选的)
# 指定账号执行,无论是否设置了随机延迟,均立即运行
task <file_path> desi <env_name> <account_number>
# 设置任务超时时间
task -m <max_time> <file_path>
# 使用 -- 分割,-- 后面的参数会传给脚本,下面的例子,脚本就可接收到参数 -u whyour -p password
task <file_path> -- -u whyour -p password
```
- ql
```bash
# 更新并重启青龙
ql update
# 运行自定义脚本extra.sh
ql extra
# 添加单个脚本文件
ql raw <file_url>
# 添加单个仓库的指定脚本
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch> <extensions>
# 删除旧日志
ql rmlog <days>
# 启动tg-bot
ql bot
# 检测青龙环境并修复
ql check
# 重置登录错误次数
ql resetlet
# 禁用两步登录
ql resettfa
```
| **参数** | **说明** |
|------------|---------------------------------------------------------------------------------------------|
| file_url | 脚本地址 |
| repo_url | 仓库地址 |
| whitelist | 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割 |
| blacklist | 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割 |
| dependence | 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割 |
| extensions | 拉取仓库的文件后缀,多个竖线分割 |
| branch | 拉取仓库的分支 |
| days | 需要保留的日志的天数 |
| file_path | 任务执行时的文件路径 |
## 开发 ## 开发
+5
View File
@@ -0,0 +1,5 @@
## Reporting a Vulnerability
To report a vulnerability, please open a private vulnerability report at <https://github.com/whyour/qinglong/security>.
While the discovery of new vulnerabilities is rare, we also recommend always using the latest versions of Qinglong to ensure your application remains as secure as possible.
+11 -37
View File
@@ -4,7 +4,8 @@ import { Logger } from 'winston';
import CronService from '../services/cron'; import CronService from '../services/cron';
import CronViewService from '../services/cronView'; import CronViewService from '../services/cronView';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import cron_parser from 'cron-parser'; import { commonCronSchema } from '../validation/schedule';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
@@ -170,27 +171,14 @@ export default (app: Router) => {
route.post( route.post(
'/', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object(commonCronSchema),
command: Joi.string().required(),
schedule: Joi.string().required(),
name: Joi.string().optional(),
labels: Joi.array().optional(),
sub_id: Joi.number().optional().allow(null),
extra_schedules: Joi.array().optional().allow(null),
task_before: Joi.string().optional().allow('').allow(null),
task_after: Joi.string().optional().allow('').allow(null),
}),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
if (cron_parser.parseExpression(req.body.schedule).hasNext()) { const cronService = Container.get(CronService);
const cronService = Container.get(CronService); const data = await cronService.create(req.body);
const data = await cronService.create(req.body); return res.send({ code: 200, data });
return res.send({ code: 200, data });
} else {
return res.send({ code: 400, message: 'param schedule error' });
}
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
@@ -331,30 +319,16 @@ export default (app: Router) => {
'/', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
labels: Joi.array().optional().allow(null), ...commonCronSchema,
command: Joi.string().required(),
schedule: Joi.string().required(),
name: Joi.string().optional().allow(null),
sub_id: Joi.number().optional().allow(null),
extra_schedules: Joi.array().optional().allow(null),
task_before: Joi.string().optional().allow('').allow(null),
task_after: Joi.string().optional().allow('').allow(null),
id: Joi.number().required(), id: Joi.number().required(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
if ( const cronService = Container.get(CronService);
!req.body.schedule || const data = await cronService.update(req.body);
cron_parser.parseExpression(req.body.schedule).hasNext() return res.send({ code: 200, data });
) {
const cronService = Container.get(CronService);
const data = await cronService.update(req.body);
return res.send({ code: 200, data });
} else {
return res.send({ code: 400, message: 'param schedule error' });
}
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
@@ -418,7 +392,7 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const data = await cronService.import_crontab(); const data = await cronService.importCrontab();
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
return next(e); return next(e);
+22 -17
View File
@@ -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);
+27
View File
@@ -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,
});
}
});
};
+4
View File
@@ -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;
}; };
+67 -24
View File
@@ -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, 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,20 +34,19 @@ 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: content }); res.send({ code: 200, data: removeAnsi(content) });
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
@@ -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);
}
},
);
}; };
+123 -66
View File
@@ -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,16 @@ export default (app: Router) => {
route.post( route.post(
'/', '/',
upload.single('file'), upload.single('file'),
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().optional().allow(''),
content: Joi.string().optional().allow(''),
originFilename: Joi.string().optional().allow(''),
directory: Joi.string().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
let { filename, path, content, originFilename, directory } = let { filename, path, content, originFilename, directory } =
req.body as { req.body as {
@@ -124,8 +153,8 @@ export default (app: Router) => {
} }
if (config.writePathList.every((x) => !path.startsWith(x))) { if (config.writePathList.every((x) => !path.startsWith(x))) {
return res.send({ return res.send({
code: 430, code: 403,
message: '文件路径禁止访问', message: '暂无权限',
}); });
} }
@@ -175,14 +204,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 +231,27 @@ export default (app: Router) => {
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().allow(''), path: Joi.string().optional().allow(''),
type: Joi.string().optional(), type: Joi.string().optional(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
let { filename, path, type } = req.body as { let { filename, path } = req.body as {
filename: string; filename: string;
path: string; path: string;
type: string;
}; };
const filePath = join(config.scriptPath, path, filename); if (!path) {
path = '';
}
const scriptService = Container.get(ScriptService);
const filePath = scriptService.checkFilePath(path, filename);
if (!filePath) {
return res.send({
code: 403,
message: '暂无权限',
});
}
await rmPath(filePath); await rmPath(filePath);
res.send({ code: 200 }); res.send({ code: 200 });
} catch (e) { } catch (e) {
@@ -222,24 +265,30 @@ export default (app: Router) => {
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().optional().allow(''),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
let { filename } = req.body as { let { filename, path } = req.body as {
filename: string; filename: string;
path: string;
}; };
const filePath = join(config.scriptPath, filename); if (!path) {
// const stats = fs.statSync(filePath); path = '';
// res.set({ }
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件 const scriptService = Container.get(ScriptService);
// 'Content-Disposition': 'attachment; filename=' + filename, //告诉浏览器这是一个需要下载的文件 const filePath = scriptService.checkFilePath(path, filename);
// 'Content-Length': stats.size //文件大小 if (!filePath) {
// }); return res.send({
// fs.createReadStream(filePath).pipe(res); code: 403,
message: '暂无权限',
});
}
return res.download(filePath, filename, (err) => { return res.download(filePath, filename, (err) => {
return next(err); if (err) {
return next(err);
}
}); });
} catch (e) { } catch (e) {
return next(e); return next(e);
@@ -260,6 +309,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 +337,9 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
let { filename, path, pid } = req.body; let { filename, path, pid } = req.body;
if (!path) {
path = '';
}
const { name, ext } = parse(filename); const { name, ext } = parse(filename);
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`); const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
const logPath = join(config.logPath, path, `${name}.swap`); const logPath = join(config.logPath, path, `${name}.swap`);
@@ -312,12 +367,14 @@ export default (app: Router) => {
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
let { filename, path, type, newFilename } = req.body as { let { filename, path, newFilename } = req.body as {
filename: string; filename: string;
path: string; path: string;
type: string;
newFilename: string; newFilename: string;
}; };
if (!path) {
path = '';
}
const filePath = join(config.scriptPath, path, filename); const filePath = join(config.scriptPath, path, filename);
const newPath = join(config.scriptPath, path, newFilename); const newPath = join(config.scriptPath, path, newFilename);
await fs.rename(filePath, newPath); await fs.rename(filePath, newPath);
+49 -5
View File
@@ -273,19 +273,20 @@ export default (app: Router) => {
{ {
onStart: async (cp, startTime) => { onStart: async (cp, startTime) => {
res.setHeader('QL-Task-Pid', `${cp.pid}`); res.setHeader('QL-Task-Pid', `${cp.pid}`);
res.setHeader('QL-Task-Log', `${logPath}`);
}, },
onEnd: async (cp, endTime, diff) => { onEnd: async (cp, endTime, diff) => {
res.end(); res.end();
}, },
onError: async (message: string) => { onError: async (message: string) => {
res.write(`\n${message}`); res.write(message);
const absolutePath = await handleLogPath(logPath); const absolutePath = await handleLogPath(logPath);
await fs.appendFile(absolutePath, `\n${message}`); await fs.appendFile(absolutePath, message);
}, },
onLog: async (message: string) => { onLog: async (message: string) => {
res.write(`\n${message}`); res.write(message);
const absolutePath = await handleLogPath(logPath); const absolutePath = await handleLogPath(logPath);
await fs.appendFile(absolutePath, `\n${message}`); await fs.appendFile(absolutePath, message);
}, },
}, },
); );
@@ -316,10 +317,15 @@ export default (app: Router) => {
route.put( route.put(
'/data/export', '/data/export',
celebrate({
body: Joi.object({
type: Joi.array().items(Joi.string()).optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
const systemService = Container.get(SystemService); const systemService = Container.get(SystemService);
await systemService.exportData(res); await systemService.exportData(res, req.body.type);
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
@@ -384,6 +390,8 @@ export default (app: Router) => {
body: Joi.object({ body: Joi.object({
retries: Joi.number().optional(), retries: Joi.number().optional(),
twoFactorActivated: Joi.boolean().optional(), twoFactorActivated: Joi.boolean().optional(),
password: Joi.string().optional(),
username: Joi.string().optional(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -396,4 +404,40 @@ export default (app: Router) => {
} }
}, },
); );
route.put(
'/config/timezone',
celebrate({
body: Joi.object({
timezone: Joi.string().allow('').allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.updateTimezone(req.body);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/config/dependence-clean',
celebrate({
body: Joi.object({
type: Joi.string().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.cleanDependence(req.body.type);
res.send(result);
} catch (e) {
return next(e);
}
},
);
}; };
+51
View File
@@ -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);
}
},
);
};
+2 -2
View File
@@ -8,6 +8,7 @@ import path from 'path';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
import rateLimit from 'express-rate-limit'; import rateLimit from 'express-rate-limit';
import config from '../config'; import config from '../config';
import { isDemoEnv } from '../config/util';
const route = Router(); const route = Router();
const storage = multer.diskStorage({ const storage = multer.diskStorage({
@@ -72,9 +73,8 @@ export default (app: Router) => {
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
if (process.env.DeployEnv === 'demo') { if (isDemoEnv()) {
return res.send({ code: 450, message: '未知错误' }); return res.send({ code: 450, message: '未知错误' });
} }
const userService = Container.get(UserService); const userService = Container.get(UserService);
+237 -26
View File
@@ -1,31 +1,242 @@
import './loaders/sentry' import 'reflect-metadata';
import 'reflect-metadata'; // We need this in order to use @Decorators import cluster, { type Worker } from 'cluster';
import config from './config'; 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');
})
.on('error', (err) => {
Logger.error(err);
console.error(err);
process.exit(1);
});
await require('./loaders/server').default({ server });
} }
startServer(); class Application {
private app: express.Application;
private httpServerService?: HttpServerService;
private grpcServerService?: GrpcServerService;
private isShuttingDown = false;
private workerMetadataMap = new Map<number, WorkerMetadata>();
constructor() {
this.app = express();
// 创建一个全局中间件,删除查询参数中的t
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
if (req.query.t) {
delete req.query.t;
}
next();
});
}
async start() {
try {
if (cluster.isPrimary) {
await this.initializeDatabase();
}
if (cluster.isPrimary) {
this.startMasterProcess();
} else {
await this.startWorkerProcess();
}
} catch (error) {
Logger.error('Failed to start application:', error);
process.exit(1);
}
}
private startMasterProcess() {
this.forkWorker('http');
this.forkWorker('grpc');
cluster.on('exit', (worker, code, signal) => {
const metadata = this.workerMetadataMap.get(worker.id);
if (metadata) {
if (!this.isShuttingDown) {
Logger.error(
`${metadata.serviceType} worker ${worker.process.pid} died (${signal || code
}). Restarting...`,
);
const newWorker = this.forkWorker(metadata.serviceType);
Logger.info(
`Restarted ${metadata.serviceType} worker (New PID: ${newWorker.process.pid})`,
);
}
this.workerMetadataMap.delete(worker.id);
}
});
this.setupMasterShutdown();
}
private forkWorker(serviceType: string): Worker {
const worker = cluster.fork({ SERVICE_TYPE: serviceType });
this.workerMetadataMap.set(worker.id, {
id: worker.id,
pid: worker.process.pid!,
serviceType,
startTime: new Date(),
});
return worker;
}
private async initializeDatabase() {
const dbLoader = await import('./loaders/db');
await dbLoader.default();
}
private setupMiddlewares() {
this.app.use(helmet({
contentSecurityPolicy: false,
}));
this.app.use(cors(config.cors));
this.app.use(compression());
this.app.use(monitoringMiddleware);
}
private setupMasterShutdown() {
const shutdown = async () => {
if (this.isShuttingDown) return;
this.isShuttingDown = true;
const workers = Object.values(cluster.workers || {});
const workerPromises: Promise<void>[] = [];
workers.forEach((worker) => {
if (worker) {
const exitPromise = new Promise<void>((resolve) => {
worker.once('exit', () => {
Logger.info(`Worker ${worker.process.pid} exited`);
resolve();
});
try {
worker.send('shutdown');
} catch (error) {
Logger.warn(
`Failed to send shutdown to worker ${worker.process.pid}:`,
error,
);
}
});
workerPromises.push(exitPromise);
}
});
try {
await Promise.race([
Promise.all(workerPromises),
new Promise<void>((resolve) => {
setTimeout(() => {
Logger.warn('Worker shutdown timeout reached');
resolve();
}, 10000);
}),
]);
process.exit(0);
} catch (error) {
Logger.error('Error during worker shutdown:', error);
process.exit(1);
}
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
}
private async startWorkerProcess() {
const serviceType = process.env.SERVICE_TYPE;
if (!serviceType || !['http', 'grpc'].includes(serviceType)) {
Logger.error('Invalid SERVICE_TYPE:', serviceType);
process.exit(1);
}
Logger.info(`✌️ ${serviceType} worker started (PID: ${process.pid})`);
try {
if (serviceType === 'http') {
await this.startHttpService();
} else {
await this.startGrpcService();
}
process.send?.('ready');
} catch (error) {
Logger.error(`${serviceType} worker failed:`, error);
process.exit(1);
}
}
private async startHttpService() {
this.setupMiddlewares();
const { HttpServerService } = await import('./services/http');
this.httpServerService = Container.get(HttpServerService);
const appLoader = await import('./loaders/app');
await appLoader.default({ app: this.app });
const server = await this.httpServerService.initialize(
this.app,
config.port,
);
const serverLoader = await import('./loaders/server');
await (serverLoader.default as any)({ server });
this.setupWorkerShutdown('http');
}
private async startGrpcService() {
const { GrpcServerService } = await import('./services/grpc');
this.grpcServerService = Container.get(GrpcServerService);
await this.grpcServerService.initialize();
this.setupWorkerShutdown('grpc');
}
private setupWorkerShutdown(serviceType: string) {
process.on('message', (msg) => {
if (msg === 'shutdown') {
this.gracefulShutdown(serviceType);
}
});
const shutdown = () => this.gracefulShutdown(serviceType);
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
}
private async gracefulShutdown(serviceType: string) {
if (this.isShuttingDown) return;
this.isShuttingDown = true;
try {
if (serviceType === 'http') {
await this.httpServerService?.shutdown();
} else {
await this.grpcServerService?.shutdown();
}
process.exit(0);
} catch (error) {
Logger.error(`[${serviceType}] Error during shutdown:`, error);
process.exit(1);
}
}
}
const app = new Application();
app.start().catch((error) => {
Logger.error('Application failed to start:', error);
process.exit(1);
});
+26
View File
@@ -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;
+68
View File
@@ -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,
};
+57 -17
View File
@@ -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,12 +86,13 @@ const dbPath = path.join(dataPath, 'db/');
const uploadPath = path.join(dataPath, 'upload/'); const uploadPath = path.join(dataPath, 'upload/');
const sshdPath = path.join(dataPath, 'ssh.d/'); const sshdPath = path.join(dataPath, 'ssh.d/');
const systemLogPath = path.join(dataPath, 'syslog/'); const systemLogPath = path.join(dataPath, 'syslog/');
const dependenceCachePath = path.join(dataPath, 'dep_cache/');
const envFile = path.join(preloadPath, 'env.sh'); const envFile = path.join(preloadPath, 'env.sh');
const jsEnvFile = path.join(preloadPath, 'env.js'); const jsEnvFile = path.join(preloadPath, 'env.js');
const pyEnvFile = path.join(preloadPath, 'env.py'); const pyEnvFile = path.join(preloadPath, 'env.py');
const jsNotifyFile = path.join(preloadPath, 'notify.js'); const jsNotifyFile = path.join(preloadPath, '__ql_notify__.js');
const pyNotifyFile = path.join(preloadPath, 'notify.py'); const pyNotifyFile = path.join(preloadPath, '__ql_notify__.py');
const confFile = path.join(configPath, 'config.sh'); const confFile = path.join(configPath, 'config.sh');
const crontabFile = path.join(configPath, 'crontab.list'); const crontabFile = path.join(configPath, 'crontab.list');
const authConfigFile = path.join(configPath, 'auth.json'); const authConfigFile = path.join(configPath, 'auth.json');
@@ -51,7 +100,6 @@ const extraFile = path.join(configPath, 'extra.sh');
const confBakDir = path.join(dataPath, 'config/bak/'); const confBakDir = path.join(dataPath, 'config/bak/');
const sampleFile = path.join(samplePath, 'config.sample.sh'); const sampleFile = path.join(samplePath, 'config.sample.sh');
const sqliteFile = path.join(samplePath, 'database.sqlite'); const sqliteFile = path.join(samplePath, 'database.sqlite');
const systemNotifyFile = path.join(preloadPath, 'system-notify.json');
const authError = '错误的用户名密码,请重试'; const authError = '错误的用户名密码,请重试';
const loginFaild = '请先登录!'; const loginFaild = '请先登录!';
@@ -66,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,
@@ -119,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',
@@ -135,5 +175,5 @@ export default {
sqliteFile, sqliteFile,
sshdPath, sshdPath,
systemLogPath, systemLogPath,
systemNotifyFile, dependenceCachePath,
}; };
+140 -104
View File
@@ -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';
@@ -22,6 +21,10 @@ export async function getFileContentByName(fileName: string) {
return ''; return '';
} }
export function removeAnsi(text: string) {
return text.replace(/\x1b\[\d+m/g, '');
}
export async function getLastModifyFilePath(dir: string) { export async function getLastModifyFilePath(dir: string) {
let filePath = ''; let filePath = '';
@@ -53,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);
@@ -153,7 +84,12 @@ export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
let platform = 'desktop'; let platform = 'desktop';
if (system === 'windows' || system === 'macos' || system === 'linux') { if (system === 'windows' || system === 'macos' || system === 'linux') {
platform = 'desktop'; platform = 'desktop';
} else if (system === 'android' || system === 'ios' || system === 'openharmony' || testUa(/mobile/g)) { } else if (
system === 'android' ||
system === 'ios' ||
system === 'openharmony' ||
testUa(/mobile/g)
) {
platform = 'mobile'; platform = 'mobile';
} }
@@ -228,19 +164,19 @@ enum FileType {
'file', 'file',
} }
interface IFile { export interface IFile {
title: string; title: string;
key: string; key: string;
type: 'directory' | 'file'; type: 'directory' | 'file';
parent: string; parent: string;
mtime: number; createTime: number;
size?: number; size?: number;
children?: IFile[]; children?: IFile[];
} }
export function dirSort(a: IFile, b: IFile): number { export function dirSort(a: IFile, b: IFile): number {
if (a.type === 'file' && b.type === 'file') { if (a.type === 'file' && b.type === 'file') {
return b.mtime - a.mtime; return b.createTime - a.createTime;
} else if (a.type === 'directory' && b.type === 'directory') { } else if (a.type === 'directory' && b.type === 'directory') {
return a.title.localeCompare(b.title); return a.title.localeCompare(b.title);
} else { } else {
@@ -274,7 +210,7 @@ export async function readDirs(
key, key,
type: 'directory', type: 'directory',
parent: relativePath, parent: relativePath,
mtime: stats.mtime.getTime(), createTime: stats.birthtime.getTime(),
children: children.sort(sort), children: children.sort(sort),
}); });
} else { } else {
@@ -284,7 +220,7 @@ export async function readDirs(
key, key,
parent: relativePath, parent: relativePath,
size: stats.size, size: stats.size,
mtime: stats.mtime.getTime(), createTime: stats.birthtime.getTime(),
}); });
} }
} }
@@ -296,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> {
@@ -343,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) {
@@ -424,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)));
}); });
}); });
} }
@@ -518,7 +482,7 @@ export function safeJSONParse(value?: string) {
try { try {
return JSON.parse(value); return JSON.parse(value);
} catch (error) { } catch (error) {
Logger.error('[JSON.parse失败]', error); Logger.error('[safeJSONParse失败]', error);
return {}; return {};
} }
} }
@@ -533,3 +497,75 @@ export async function rmPath(path: string) {
Logger.error('[rmPath失败]', error); Logger.error('[rmPath失败]', error);
} }
} }
export async function setSystemTimezone(timezone: string): Promise<boolean> {
try {
if (!(await fileExist(`/usr/share/zoneinfo/${timezone}`))) {
throw new Error('Invalid timezone');
}
await promiseExec(`ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`);
await promiseExec(`echo "${timezone}" > /etc/timezone`);
return true;
} catch (error) {
Logger.error('[setSystemTimezone失败]', error);
return false;
}
}
export function getGetCommand(type: DependenceTypes, name: string): string {
const baseCommands = {
[DependenceTypes.nodejs]: `pnpm ls -g | grep "${name}" | head -1`,
[DependenceTypes.python3]: `
python3 -c "exec('''
name='${name}'
try:
from importlib.metadata import version
print(version(name))
except:
import importlib.util as u
import importlib.metadata as m
spec=u.find_spec(name)
print(name if spec else '')
''')"`,
[DependenceTypes.linux]: `apk info -es ${name}`,
};
return baseCommands[type];
}
export function getInstallCommand(type: DependenceTypes, name: string): string {
const baseCommands = {
[DependenceTypes.nodejs]: 'pnpm add -g',
[DependenceTypes.python3]:
'pip3 install --disable-pip-version-check --root-user-action=ignore',
[DependenceTypes.linux]: 'apk add --no-check-certificate',
};
let command = baseCommands[type];
if (type === DependenceTypes.python3 && PYTHON_INSTALL_DIR) {
command = `${command} --prefix=${PYTHON_INSTALL_DIR}`;
}
return `${command} ${name.trim()}`;
}
export function getUninstallCommand(
type: DependenceTypes,
name: string,
): string {
const baseCommands = {
[DependenceTypes.nodejs]: 'pnpm remove -g',
[DependenceTypes.python3]:
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
[DependenceTypes.linux]: 'apk del',
};
return `${baseCommands[type]} ${name.trim()}`;
}
export function isDemoEnv() {
return process.env.DeployEnv === 'demo';
}
-18
View File
@@ -41,30 +41,12 @@ export enum DependenceTypes {
'linux', 'linux',
} }
export enum InstallDependenceCommandTypes {
'pnpm add -g',
'pip3 install --disable-pip-version-check --root-user-action=ignore',
'apk add --no-check-certificate',
}
export enum GetDependenceCommandTypes {
'pnpm ls -g ',
'pip3 show --disable-pip-version-check',
'apk info -es',
}
export enum versionDependenceCommandTypes { export enum versionDependenceCommandTypes {
'@', '@',
'==', '==',
'=', '=',
} }
export enum unInstallDependenceCommandTypes {
'pnpm remove -g',
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
'apk del',
}
export interface DependenceInstance export interface DependenceInstance
extends Model<Dependence, Dependence>, extends Model<Dependence, Dependence>,
Dependence {} Dependence {}
+1 -1
View File
@@ -20,7 +20,7 @@ export class Env {
this.timestamp = new Date().toString(); this.timestamp = new Date().toString();
this.position = options.position; this.position = options.position;
this.name = options.name; this.name = options.name;
this.remarks = options.remarks; this.remarks = options.remarks || '';
} }
} }
+9 -7
View File
@@ -1,5 +1,3 @@
import { IncomingHttpHeaders } from 'http';
export enum NotificationMode { export enum NotificationMode {
'gotify' = 'gotify', 'gotify' = 'gotify',
'goCqHttpBot' = 'goCqHttpBot', 'goCqHttpBot' = 'goCqHttpBot',
@@ -49,9 +47,8 @@ export class PushDeerNotification extends NotificationBaseInfo {
public pushDeerUrl = ''; public pushDeerUrl = '';
} }
export class ChatNotification extends NotificationBaseInfo { export class synologyChatNotification extends NotificationBaseInfo {
public chatUrl = ''; public synologyChatUrl = '';
public chatToken = '';
} }
export class BarkNotification extends NotificationBaseInfo { export class BarkNotification extends NotificationBaseInfo {
@@ -61,7 +58,7 @@ export class BarkNotification extends NotificationBaseInfo {
public barkGroup = 'qinglong'; public barkGroup = 'qinglong';
public barkLevel = 'active'; public barkLevel = 'active';
public barkUrl = ''; public barkUrl = '';
public barkArchive="" public barkArchive = '';
} }
export class TelegramBotNotification extends NotificationBaseInfo { export class TelegramBotNotification extends NotificationBaseInfo {
@@ -118,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 {
@@ -150,6 +148,10 @@ 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 {
@@ -163,7 +165,7 @@ export interface NotificationInfo
GotifyNotification, GotifyNotification,
ServerChanNotification, ServerChanNotification,
PushDeerNotification, PushDeerNotification,
ChatNotification, synologyChatNotification,
BarkNotification, BarkNotification,
TelegramBotNotification, TelegramBotNotification,
DingtalkBotNotification, DingtalkBotNotification,
+1
View File
@@ -37,6 +37,7 @@ export interface SystemConfigInfo {
nodeMirror?: string; nodeMirror?: string;
pythonMirror?: string; pythonMirror?: string;
linuxMirror?: string; linuxMirror?: string;
timezone?: string;
} }
export interface LoginLogInfo { export interface LoginLogInfo {
-7
View File
@@ -1,7 +0,0 @@
declare namespace Express {
interface Request {
platform: 'desktop' | 'mobile';
}
}
declare module 'pstree.remy';
+13
View File
@@ -0,0 +1,13 @@
export enum ScheduleType {
BOOT = '@boot',
ONCE = '@once',
}
export type ScheduleValidator = (schedule?: string) => boolean;
export type CronSchedulerPayload = {
name: string;
id: string;
schedule: string;
command: string;
extra_schedules: Array<{ schedule: string }>;
};
+9 -10
View File
@@ -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');
}; };
+8
View File
@@ -0,0 +1,8 @@
import Container from 'typedi';
import CronService from '../services/cron';
export default async () => {
const cronService = Container.get(CronService);
await cronService.bootTask();
};
+8 -84
View File
@@ -1,15 +1,11 @@
import Logger from './logger'; import Logger from './logger';
import path from 'path';
import DataStore from 'nedb';
import { EnvModel } from '../data/env'; import { EnvModel } from '../data/env';
import { CrontabModel } from '../data/cron'; import { CrontabModel } from '../data/cron';
import { DependenceModel } from '../data/dependence'; import { DependenceModel } from '../data/dependence';
import { AppModel } from '../data/open'; import { AppModel } from '../data/open';
import { SystemModel } from '../data/system'; import { SystemModel } from '../data/system';
import { fileExist } from '../config/util';
import { SubscriptionModel } from '../data/subscription'; import { SubscriptionModel } from '../data/subscription';
import { CrontabViewModel } from '../data/cronView'; import { CrontabViewModel } from '../data/cronView';
import config from '../config';
import { sequelize } from '../data'; import { sequelize } from '../data';
export default async () => { export default async () => {
@@ -48,93 +44,21 @@ export default async () => {
} catch (error) {} } catch (error) {}
try { try {
await sequelize.query('alter table Crontabs add column sub_id NUMBER'); await sequelize.query('alter table Crontabs add column sub_id NUMBER');
} catch (error) { } } catch (error) {}
try { try {
await sequelize.query('alter table Crontabs add column extra_schedules JSON'); await sequelize.query(
} catch (error) { } 'alter table Crontabs add column extra_schedules JSON',
);
} catch (error) {}
try { try {
await sequelize.query('alter table Crontabs add column task_before TEXT'); await sequelize.query('alter table Crontabs add column task_before TEXT');
} catch (error) { } } catch (error) {}
try { try {
await sequelize.query('alter table Crontabs add column task_after TEXT'); await sequelize.query('alter table Crontabs add column task_after TEXT');
} catch (error) { } } catch (error) {}
// 2.10-2.11 升级
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
const envDbFile = path.join(config.rootPath, 'db/env.db');
const appDbFile = path.join(config.rootPath, 'db/app.db');
const authDbFile = path.join(config.rootPath, 'db/auth.db');
const dependenceDbFile = path.join(config.rootPath, 'db/dependence.db');
const crondbExist = await fileExist(cronDbFile);
const dependenceDbExist = await fileExist(dependenceDbFile);
const envDbExist = await fileExist(envDbFile);
const appDbExist = await fileExist(appDbFile);
const authDbExist = await fileExist(authDbFile);
const cronCount = await CrontabModel.count();
const dependenceCount = await DependenceModel.count();
const envCount = await EnvModel.count();
const appCount = await AppModel.count();
const authCount = await SystemModel.count();
if (crondbExist && cronCount === 0) {
const cronDb = new DataStore({
filename: cronDbFile,
autoload: true,
});
cronDb.persistence.compactDatafile();
cronDb.find({}).exec(async (err, docs) => {
await CrontabModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
if (dependenceDbExist && dependenceCount === 0) {
const dependenceDb = new DataStore({
filename: dependenceDbFile,
autoload: true,
});
dependenceDb.persistence.compactDatafile();
dependenceDb.find({}).exec(async (err, docs) => {
await DependenceModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
if (envDbExist && envCount === 0) {
const envDb = new DataStore({
filename: envDbFile,
autoload: true,
});
envDb.persistence.compactDatafile();
envDb.find({}).exec(async (err, docs) => {
await EnvModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
if (appDbExist && appCount === 0) {
const appDb = new DataStore({
filename: appDbFile,
autoload: true,
});
appDb.persistence.compactDatafile();
appDb.find({}).exec(async (err, docs) => {
await AppModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
if (authDbExist && authCount === 0) {
const authDb = new DataStore({
filename: authDbFile,
autoload: true,
});
authDb.persistence.compactDatafile();
authDb.find({}).exec(async (err, docs) => {
await SystemModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
console.log('✌️ DB loaded');
Logger.info('✌️ DB loaded'); Logger.info('✌️ DB loaded');
} catch (error) { } catch (error) {
console.error('✌️ DB load failed'); Logger.error('✌️ DB load failed', error);
Logger.error(error);
} }
}; };
+7 -1
View File
@@ -13,7 +13,7 @@ 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() {
@@ -36,6 +36,12 @@ async function linkCommand() {
const source = path.join(config.rootPath, 'shell', link.src); const source = path.join(config.rootPath, 'shell', link.src);
const target = path.join(commandDir, link.dest); const target = path.join(commandDir, link.dest);
const tmpTarget = path.join(commandDir, link.tmp); const tmpTarget = path.join(commandDir, link.tmp);
try {
const stats = await fs.lstat(tmpTarget);
if (stats) {
await fs.unlink(tmpTarget);
}
} catch (error) { }
await fs.symlink(source, tmpTarget); await fs.symlink(source, tmpTarget);
await fs.rename(tmpTarget, target); await fs.rename(tmpTarget, target);
} }
+22 -24
View File
@@ -6,12 +6,10 @@ import config from '../config';
import { UnauthorizedError, expressjwt } from 'express-jwt'; 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 * as Sentry from '@sentry/node';
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 path from 'path';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
app.set('trust proxy', 'loopback'); app.set('trust proxy', 'loopback');
@@ -19,25 +17,18 @@ export default ({ app }: { app: Application }) => {
app.get(`${config.api.prefix}/env.js`, serveEnv); app.get(`${config.api.prefix}/env.js`, serveEnv);
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath)); app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
app.use(
'/api/public',
createProxyMiddleware({
target: `http://0.0.0.0:${config.publicPort}/api`,
changeOrigin: true,
pathRewrite: { '/api/public': '' },
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\/).*/],
}), }),
); );
@@ -51,7 +42,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();
@@ -123,16 +118,19 @@ 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());
Sentry.setupExpressErrorHandler(app);
app.use( app.use(
( (
err: Error & { status: number }, err: Error & { status: number },
@@ -162,8 +160,8 @@ export default ({ app }: { app: Application }) => {
.status(500) .status(500)
.send({ .send({
code: 400, code: 400,
message: `${err.name} ${err.message}`, message: `${err.message}`,
validation: err.errors, errors: err.errors,
}) })
.end(); .end();
} }
+35 -20
View File
@@ -13,10 +13,11 @@ 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 { 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';
import { AppModel } from '../data/open';
export default async () => { export default async () => {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
@@ -27,10 +28,23 @@ export default async () => {
const openService = Container.get(OpenService); const openService = Container.get(OpenService);
// 初始化增加系统配置 // 初始化增加系统配置
let systemApp = (
await AppModel.findOne({
where: { name: 'system' },
})
)?.get({ plain: true });
if (!systemApp) {
systemApp = await AppModel.create({
name: 'system',
scopes: ['crons', 'system'],
client_id: createRandomString(12, 12),
client_secret: createRandomString(24, 24),
});
}
const [systemConfig] = await SystemModel.findOrCreate({ const [systemConfig] = await SystemModel.findOrCreate({
where: { type: AuthDataType.systemConfig }, where: { type: AuthDataType.systemConfig },
}); });
const [notifyConfig] = await SystemModel.findOrCreate({ await SystemModel.findOrCreate({
where: { type: AuthDataType.notification }, where: { type: AuthDataType.notification },
}); });
const [authConfig] = await SystemModel.findOrCreate({ const [authConfig] = await SystemModel.findOrCreate({
@@ -42,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');
} }
@@ -54,29 +71,27 @@ export default async () => {
}); });
} }
// 初始化通知配置 const installDependencies = async () => {
if (notifyConfig.info) { const docs = await DependenceModel.findAll({
await writeFile(config.systemNotifyFile, JSON.stringify(notifyConfig.info));
}
const installDependencies = () => {
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
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 镜像源配置
+5 -4
View File
@@ -27,8 +27,8 @@ const sampleNotifyJsFile = path.join(samplePath, 'notify.js');
const sampleNotifyPyFile = path.join(samplePath, 'notify.py'); const sampleNotifyPyFile = path.join(samplePath, 'notify.py');
const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js'); const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js');
const scriptNotifyPyFile = path.join(scriptPath, 'notify.py'); const scriptNotifyPyFile = path.join(scriptPath, 'notify.py');
const jsNotifyFile = path.join(preloadPath, 'notify.js'); const jsNotifyFile = path.join(preloadPath, '__ql_notify__.js');
const pyNotifyFile = path.join(preloadPath, 'notify.py'); const pyNotifyFile = path.join(preloadPath, '__ql_notify__.py');
const TaskBeforeFile = path.join(configPath, 'task_before.sh'); const TaskBeforeFile = path.join(configPath, 'task_before.sh');
const TaskBeforeJsFile = path.join(configPath, 'task_before.js'); const TaskBeforeJsFile = path.join(configPath, 'task_before.js');
const TaskBeforePyFile = path.join(configPath, 'task_before.py'); const TaskBeforePyFile = path.join(configPath, 'task_before.py');
@@ -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');
}; };
+19 -15
View File
@@ -38,21 +38,25 @@ export default async () => {
// 运行删除日志任务 // 运行删除日志任务
const data = await systemService.getSystemConfig(); const data = await systemService.getSystemConfig();
if (data && data.info && data.info.logRemoveFrequency) { if (data && data.info) {
const rmlogCron = { if (data.info.logRemoveFrequency) {
id: data.id as number, const rmlogCron = {
name: '删除日志', id: data.id as number,
command: `ql rmlog ${data.info.logRemoveFrequency}`, name: '删除日志',
runOrigin: 'system' as const, command: `ql rmlog ${data.info.logRemoveFrequency}`,
}; runOrigin: 'system' as const,
await scheduleService.cancelIntervalTask(rmlogCron); };
scheduleService.createIntervalTask( await scheduleService.cancelIntervalTask(rmlogCron);
rmlogCron, scheduleService.createIntervalTask(
{ rmlogCron,
days: data.info.logRemoveFrequency, {
}, days: data.info.logRemoveFrequency,
true, },
); true,
);
}
systemService.updateTimezone(data.info);
} }
await subscriptionService.setSshConfig(); await subscriptionService.setSshConfig();
+50 -25
View File
@@ -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;
-26
View File
@@ -1,26 +0,0 @@
import * as Sentry from '@sentry/node';
import Logger from './logger';
import fs from 'fs';
import config from '../config';
import { parseContentVersion } from '../config/util';
let version = '1.0.0';
try {
const content = fs.readFileSync(config.versionFile, 'utf-8');
({ version } = parseContentVersion(content));
} catch (error) {}
Sentry.init({
ignoreErrors: [
/SequelizeUniqueConstraintError/i,
/Validation error/i,
/UnauthorizedError/i,
/celebrate request validation failed/i,
],
dsn: 'https://8b5c84cfef3e22541bc84de0ed00497b@o1098464.ingest.sentry.io/6122819',
tracesSampleRate: 0.5,
release: version,
});
Logger.info('✌️ Sentry loaded');
console.log('✌️ Sentry loaded');
-106
View File
@@ -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,
});
},
);
};
+80
View File
@@ -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),
};
};
+257
View File
@@ -0,0 +1,257 @@
syntax = "proto3";
package com.ql.api;
message EnvItem {
optional int32 id = 1;
optional string name = 2;
optional string value = 3;
optional string remarks = 4;
optional int32 status = 5;
optional int64 position = 6;
}
message GetEnvsRequest { string searchValue = 1; }
message CreateEnvRequest { repeated EnvItem envs = 1; }
message UpdateEnvRequest { EnvItem env = 1; }
message DeleteEnvsRequest { repeated int32 ids = 1; }
message MoveEnvRequest {
int32 id = 1;
int32 fromIndex = 2;
int32 toIndex = 3;
}
message DisableEnvsRequest { repeated int32 ids = 1; }
message EnableEnvsRequest { repeated int32 ids = 1; }
message UpdateEnvNamesRequest {
repeated int32 ids = 1;
string name = 2;
}
message GetEnvByIdRequest { int32 id = 1; }
message EnvsResponse {
int32 code = 1;
repeated EnvItem data = 2;
optional string message = 3;
}
message EnvResponse {
int32 code = 1;
EnvItem data = 2;
optional string message = 3;
}
message Response {
int32 code = 1;
optional string message = 2;
}
message ExtraScheduleItem { string schedule = 1; }
message CronItem {
optional int32 id = 1;
optional string command = 2;
optional string schedule = 3;
optional string name = 4;
repeated string labels = 5;
optional int32 sub_id = 6;
repeated ExtraScheduleItem extra_schedules = 7;
optional string task_before = 8;
optional string task_after = 9;
optional int32 status = 10;
optional string log_path = 11;
optional int32 pid = 12;
optional int64 last_running_time = 13;
optional int64 last_execution_time = 14;
}
message CreateCronRequest {
string command = 1;
string schedule = 2;
optional string name = 3;
repeated string labels = 4;
optional int32 sub_id = 5;
repeated ExtraScheduleItem extra_schedules = 6;
optional string task_before = 7;
optional string task_after = 8;
}
message UpdateCronRequest {
int32 id = 1;
optional string command = 2;
optional string schedule = 3;
optional string name = 4;
repeated string labels = 5;
optional int32 sub_id = 6;
repeated ExtraScheduleItem extra_schedules = 7;
optional string task_before = 8;
optional string task_after = 9;
}
message DeleteCronsRequest { repeated int32 ids = 1; }
message CronsResponse {
int32 code = 1;
repeated CronItem data = 2;
optional string message = 3;
}
message CronResponse {
int32 code = 1;
CronItem data = 2;
optional string message = 3;
}
message CronDetailRequest { string log_path = 1; }
message CronDetailResponse {
int32 code = 1;
CronItem data = 2;
optional string message = 3;
}
enum NotificationMode {
gotify = 0;
goCqHttpBot = 1;
serverChan = 2;
pushDeer = 3;
bark = 4;
chat = 5;
telegramBot = 6;
dingtalkBot = 7;
weWorkBot = 8;
weWorkApp = 9;
aibotk = 10;
iGot = 11;
pushPlus = 12;
wePlusBot = 13;
email = 14;
pushMe = 15;
feishu = 16;
webhook = 17;
chronocat = 18;
ntfy = 19;
wxPusherBot = 20;
}
message NotificationInfo {
NotificationMode type = 1;
optional string gotifyUrl = 2;
optional string gotifyToken = 3;
optional int32 gotifyPriority = 4;
optional string goCqHttpBotUrl = 5;
optional string goCqHttpBotToken = 6;
optional string goCqHttpBotQq = 7;
optional string serverChanKey = 8;
optional string pushDeerKey = 9;
optional string pushDeerUrl = 10;
optional string synologyChatUrl = 11;
optional string barkPush = 12;
optional string barkIcon = 13;
optional string barkSound = 14;
optional string barkGroup = 15;
optional string barkLevel = 16;
optional string barkUrl = 17;
optional string barkArchive = 18;
optional string telegramBotToken = 19;
optional string telegramBotUserId = 20;
optional string telegramBotProxyHost = 21;
optional string telegramBotProxyPort = 22;
optional string telegramBotProxyAuth = 23;
optional string telegramBotApiHost = 24;
optional string dingtalkBotToken = 25;
optional string dingtalkBotSecret = 26;
optional string weWorkBotKey = 27;
optional string weWorkOrigin = 28;
optional string weWorkAppKey = 29;
optional string aibotkKey = 30;
optional string aibotkType = 31;
optional string aibotkName = 32;
optional string iGotPushKey = 33;
optional string pushPlusToken = 34;
optional string pushPlusUser = 35;
optional string pushPlusTemplate = 36;
optional string pushplusChannel = 37;
optional string pushplusWebhook = 38;
optional string pushplusCallbackUrl = 39;
optional string pushplusTo = 40;
optional string wePlusBotToken = 41;
optional string wePlusBotReceiver = 42;
optional string wePlusBotVersion = 43;
optional string emailService = 44;
optional string emailUser = 45;
optional string emailPass = 46;
optional string emailTo = 47;
optional string pushMeKey = 48;
optional string pushMeUrl = 49;
optional string chronocatURL = 50;
optional string chronocatQQ = 51;
optional string chronocatToken = 52;
optional string webhookHeaders = 53;
optional string webhookBody = 54;
optional string webhookUrl = 55;
optional string webhookMethod = 56;
optional string webhookContentType = 57;
optional string larkKey = 58;
optional string ntfyUrl = 59;
optional string ntfyTopic = 60;
optional string ntfyPriority = 61;
optional string ntfyToken = 62;
optional string ntfyUsername = 63;
optional string ntfyPassword = 64;
optional string ntfyActions = 65;
optional string wxPusherBotAppToken = 66;
optional string wxPusherBotTopicIds = 67;
optional string wxPusherBotUids = 68;
}
message SystemNotifyRequest {
string title = 1;
string content = 2;
optional NotificationInfo notificationInfo = 3;
}
service Api {
rpc GetEnvs(GetEnvsRequest) returns (EnvsResponse) {}
rpc CreateEnv(CreateEnvRequest) returns (EnvsResponse) {}
rpc UpdateEnv(UpdateEnvRequest) returns (EnvResponse) {}
rpc DeleteEnvs(DeleteEnvsRequest) returns (Response) {}
rpc MoveEnv(MoveEnvRequest) returns (EnvResponse) {}
rpc DisableEnvs(DisableEnvsRequest) returns (Response) {}
rpc EnableEnvs(EnableEnvsRequest) returns (Response) {}
rpc UpdateEnvNames(UpdateEnvNamesRequest) returns (Response) {}
rpc GetEnvById(GetEnvByIdRequest) returns (EnvResponse) {}
rpc SystemNotify(SystemNotifyRequest) returns (Response) {}
rpc GetCronDetail(CronDetailRequest) returns (CronDetailResponse) {}
rpc CreateCron(CreateCronRequest) returns (CronResponse) {}
rpc UpdateCron(UpdateCronRequest) returns (CronResponse) {}
rpc DeleteCrons(DeleteCronsRequest) returns (Response) {}
}
+4251
View File
File diff suppressed because it is too large Load Diff
+114 -91
View File
@@ -1,17 +1,23 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.6.1
// protoc v3.17.3
// source: back/protos/cron.proto
/* eslint-disable */ /* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
import { import {
CallOptions, type CallOptions,
ChannelCredentials, ChannelCredentials,
Client, Client,
ClientOptions, type ClientOptions,
ClientUnaryCall, type ClientUnaryCall,
handleUnaryCall, type handleUnaryCall,
makeGenericClientConstructor, makeGenericClientConstructor,
Metadata, Metadata,
ServiceError, type ServiceError,
UntypedServiceImplementation, type UntypedServiceImplementation,
} from "@grpc/grpc-js"; } from "@grpc/grpc-js";
import _m0 from "protobufjs/minimal";
export const protobufPackage = "com.ql.cron"; export const protobufPackage = "com.ql.cron";
@@ -23,7 +29,7 @@ export interface ICron {
id: string; id: string;
schedule: string; schedule: string;
command: string; command: string;
extraSchedules: ISchedule[]; extra_schedules: ISchedule[];
name: string; name: string;
} }
@@ -45,51 +51,53 @@ function createBaseISchedule(): ISchedule {
return { schedule: "" }; return { schedule: "" };
} }
export const ISchedule = { export const ISchedule: MessageFns<ISchedule> = {
encode(message: ISchedule, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(message: ISchedule, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.schedule !== "") { if (message.schedule !== "") {
writer.uint32(10).string(message.schedule); writer.uint32(10).string(message.schedule);
} }
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): ISchedule { decode(input: BinaryReader | Uint8Array, length?: number): ISchedule {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseISchedule(); const message = createBaseISchedule();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: case 1: {
if (tag !== 10) { if (tag !== 10) {
break; break;
} }
message.schedule = reader.string(); message.schedule = reader.string();
continue; continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skip(tag & 7);
} }
return message; return message;
}, },
fromJSON(object: any): ISchedule { fromJSON(object: any): ISchedule {
return { schedule: isSet(object.schedule) ? String(object.schedule) : "" }; return { schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : "" };
}, },
toJSON(message: ISchedule): unknown { toJSON(message: ISchedule): unknown {
const obj: any = {}; const obj: any = {};
message.schedule !== undefined && (obj.schedule = message.schedule); if (message.schedule !== "") {
obj.schedule = message.schedule;
}
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<ISchedule>, I>>(base?: I): ISchedule { create<I extends Exact<DeepPartial<ISchedule>, I>>(base?: I): ISchedule {
return ISchedule.fromPartial(base ?? {}); return ISchedule.fromPartial(base ?? ({} as any));
}, },
fromPartial<I extends Exact<DeepPartial<ISchedule>, I>>(object: I): ISchedule { fromPartial<I extends Exact<DeepPartial<ISchedule>, I>>(object: I): ISchedule {
const message = createBaseISchedule(); const message = createBaseISchedule();
message.schedule = object.schedule ?? ""; message.schedule = object.schedule ?? "";
@@ -98,11 +106,11 @@ export const ISchedule = {
}; };
function createBaseICron(): ICron { function createBaseICron(): ICron {
return { id: "", schedule: "", command: "", extraSchedules: [], name: "" }; return { id: "", schedule: "", command: "", extra_schedules: [], name: "" };
} }
export const ICron = { export const ICron: MessageFns<ICron> = {
encode(message: ICron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(message: ICron, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.id !== "") { if (message.id !== "") {
writer.uint32(10).string(message.id); writer.uint32(10).string(message.id);
} }
@@ -112,8 +120,8 @@ export const ICron = {
if (message.command !== "") { if (message.command !== "") {
writer.uint32(26).string(message.command); writer.uint32(26).string(message.command);
} }
for (const v of message.extraSchedules) { for (const v of message.extra_schedules) {
ISchedule.encode(v!, writer.uint32(34).fork()).ldelim(); ISchedule.encode(v!, writer.uint32(34).fork()).join();
} }
if (message.name !== "") { if (message.name !== "") {
writer.uint32(42).string(message.name); writer.uint32(42).string(message.name);
@@ -121,93 +129,103 @@ export const ICron = {
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): ICron { decode(input: BinaryReader | Uint8Array, length?: number): ICron {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseICron(); const message = createBaseICron();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: case 1: {
if (tag !== 10) { if (tag !== 10) {
break; break;
} }
message.id = reader.string(); message.id = reader.string();
continue; continue;
case 2: }
case 2: {
if (tag !== 18) { if (tag !== 18) {
break; break;
} }
message.schedule = reader.string(); message.schedule = reader.string();
continue; continue;
case 3: }
case 3: {
if (tag !== 26) { if (tag !== 26) {
break; break;
} }
message.command = reader.string(); message.command = reader.string();
continue; continue;
case 4: }
case 4: {
if (tag !== 34) { if (tag !== 34) {
break; break;
} }
message.extraSchedules.push(ISchedule.decode(reader, reader.uint32())); message.extra_schedules.push(ISchedule.decode(reader, reader.uint32()));
continue; continue;
case 5: }
case 5: {
if (tag !== 42) { if (tag !== 42) {
break; break;
} }
message.name = reader.string(); message.name = reader.string();
continue; continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skip(tag & 7);
} }
return message; return message;
}, },
fromJSON(object: any): ICron { fromJSON(object: any): ICron {
return { return {
id: isSet(object.id) ? String(object.id) : "", id: isSet(object.id) ? globalThis.String(object.id) : "",
schedule: isSet(object.schedule) ? String(object.schedule) : "", schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : "",
command: isSet(object.command) ? String(object.command) : "", command: isSet(object.command) ? globalThis.String(object.command) : "",
extraSchedules: Array.isArray(object?.extraSchedules) extra_schedules: globalThis.Array.isArray(object?.extra_schedules)
? object.extraSchedules.map((e: any) => ISchedule.fromJSON(e)) ? object.extra_schedules.map((e: any) => ISchedule.fromJSON(e))
: [], : [],
name: isSet(object.name) ? String(object.name) : "", name: isSet(object.name) ? globalThis.String(object.name) : "",
}; };
}, },
toJSON(message: ICron): unknown { toJSON(message: ICron): unknown {
const obj: any = {}; const obj: any = {};
message.id !== undefined && (obj.id = message.id); if (message.id !== "") {
message.schedule !== undefined && (obj.schedule = message.schedule); obj.id = message.id;
message.command !== undefined && (obj.command = message.command); }
if (message.extraSchedules) { if (message.schedule !== "") {
obj.extraSchedules = message.extraSchedules.map((e) => e ? ISchedule.toJSON(e) : undefined); obj.schedule = message.schedule;
} else { }
obj.extraSchedules = []; if (message.command !== "") {
obj.command = message.command;
}
if (message.extra_schedules?.length) {
obj.extra_schedules = message.extra_schedules.map((e) => ISchedule.toJSON(e));
}
if (message.name !== "") {
obj.name = message.name;
} }
message.name !== undefined && (obj.name = message.name);
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<ICron>, I>>(base?: I): ICron { create<I extends Exact<DeepPartial<ICron>, I>>(base?: I): ICron {
return ICron.fromPartial(base ?? {}); return ICron.fromPartial(base ?? ({} as any));
}, },
fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron { fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron {
const message = createBaseICron(); const message = createBaseICron();
message.id = object.id ?? ""; message.id = object.id ?? "";
message.schedule = object.schedule ?? ""; message.schedule = object.schedule ?? "";
message.command = object.command ?? ""; message.command = object.command ?? "";
message.extraSchedules = object.extraSchedules?.map((e) => ISchedule.fromPartial(e)) || []; message.extra_schedules = object.extra_schedules?.map((e) => ISchedule.fromPartial(e)) || [];
message.name = object.name ?? ""; message.name = object.name ?? "";
return message; return message;
}, },
@@ -217,55 +235,53 @@ function createBaseAddCronRequest(): AddCronRequest {
return { crons: [] }; return { crons: [] };
} }
export const AddCronRequest = { export const AddCronRequest: MessageFns<AddCronRequest> = {
encode(message: AddCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(message: AddCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
for (const v of message.crons) { for (const v of message.crons) {
ICron.encode(v!, writer.uint32(10).fork()).ldelim(); ICron.encode(v!, writer.uint32(10).fork()).join();
} }
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest { decode(input: BinaryReader | Uint8Array, length?: number): AddCronRequest {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAddCronRequest(); const message = createBaseAddCronRequest();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: case 1: {
if (tag !== 10) { if (tag !== 10) {
break; break;
} }
message.crons.push(ICron.decode(reader, reader.uint32())); message.crons.push(ICron.decode(reader, reader.uint32()));
continue; continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skip(tag & 7);
} }
return message; return message;
}, },
fromJSON(object: any): AddCronRequest { fromJSON(object: any): AddCronRequest {
return { crons: Array.isArray(object?.crons) ? object.crons.map((e: any) => ICron.fromJSON(e)) : [] }; return { crons: globalThis.Array.isArray(object?.crons) ? object.crons.map((e: any) => ICron.fromJSON(e)) : [] };
}, },
toJSON(message: AddCronRequest): unknown { toJSON(message: AddCronRequest): unknown {
const obj: any = {}; const obj: any = {};
if (message.crons) { if (message.crons?.length) {
obj.crons = message.crons.map((e) => e ? ICron.toJSON(e) : undefined); obj.crons = message.crons.map((e) => ICron.toJSON(e));
} else {
obj.crons = [];
} }
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<AddCronRequest>, I>>(base?: I): AddCronRequest { create<I extends Exact<DeepPartial<AddCronRequest>, I>>(base?: I): AddCronRequest {
return AddCronRequest.fromPartial(base ?? {}); return AddCronRequest.fromPartial(base ?? ({} as any));
}, },
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(object: I): AddCronRequest { fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(object: I): AddCronRequest {
const message = createBaseAddCronRequest(); const message = createBaseAddCronRequest();
message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || []; message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || [];
@@ -277,13 +293,13 @@ function createBaseAddCronResponse(): AddCronResponse {
return {}; return {};
} }
export const AddCronResponse = { export const AddCronResponse: MessageFns<AddCronResponse> = {
encode(_: AddCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(_: AddCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronResponse { decode(input: BinaryReader | Uint8Array, length?: number): AddCronResponse {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAddCronResponse(); const message = createBaseAddCronResponse();
while (reader.pos < end) { while (reader.pos < end) {
@@ -293,7 +309,7 @@ export const AddCronResponse = {
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skip(tag & 7);
} }
return message; return message;
}, },
@@ -308,9 +324,8 @@ export const AddCronResponse = {
}, },
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(base?: I): AddCronResponse { create<I extends Exact<DeepPartial<AddCronResponse>, I>>(base?: I): AddCronResponse {
return AddCronResponse.fromPartial(base ?? {}); return AddCronResponse.fromPartial(base ?? ({} as any));
}, },
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(_: I): AddCronResponse { fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(_: I): AddCronResponse {
const message = createBaseAddCronResponse(); const message = createBaseAddCronResponse();
return message; return message;
@@ -321,55 +336,53 @@ function createBaseDeleteCronRequest(): DeleteCronRequest {
return { ids: [] }; return { ids: [] };
} }
export const DeleteCronRequest = { export const DeleteCronRequest: MessageFns<DeleteCronRequest> = {
encode(message: DeleteCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(message: DeleteCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
for (const v of message.ids) { for (const v of message.ids) {
writer.uint32(10).string(v!); writer.uint32(10).string(v!);
} }
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronRequest { decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronRequest {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteCronRequest(); const message = createBaseDeleteCronRequest();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: case 1: {
if (tag !== 10) { if (tag !== 10) {
break; break;
} }
message.ids.push(reader.string()); message.ids.push(reader.string());
continue; continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skip(tag & 7);
} }
return message; return message;
}, },
fromJSON(object: any): DeleteCronRequest { fromJSON(object: any): DeleteCronRequest {
return { ids: Array.isArray(object?.ids) ? object.ids.map((e: any) => String(e)) : [] }; return { ids: globalThis.Array.isArray(object?.ids) ? object.ids.map((e: any) => globalThis.String(e)) : [] };
}, },
toJSON(message: DeleteCronRequest): unknown { toJSON(message: DeleteCronRequest): unknown {
const obj: any = {}; const obj: any = {};
if (message.ids) { if (message.ids?.length) {
obj.ids = message.ids.map((e) => e); obj.ids = message.ids;
} else {
obj.ids = [];
} }
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(base?: I): DeleteCronRequest { create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(base?: I): DeleteCronRequest {
return DeleteCronRequest.fromPartial(base ?? {}); return DeleteCronRequest.fromPartial(base ?? ({} as any));
}, },
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(object: I): DeleteCronRequest { fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(object: I): DeleteCronRequest {
const message = createBaseDeleteCronRequest(); const message = createBaseDeleteCronRequest();
message.ids = object.ids?.map((e) => e) || []; message.ids = object.ids?.map((e) => e) || [];
@@ -381,13 +394,13 @@ function createBaseDeleteCronResponse(): DeleteCronResponse {
return {}; return {};
} }
export const DeleteCronResponse = { export const DeleteCronResponse: MessageFns<DeleteCronResponse> = {
encode(_: DeleteCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(_: DeleteCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronResponse { decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronResponse {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteCronResponse(); const message = createBaseDeleteCronResponse();
while (reader.pos < end) { while (reader.pos < end) {
@@ -397,7 +410,7 @@ export const DeleteCronResponse = {
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skip(tag & 7);
} }
return message; return message;
}, },
@@ -412,9 +425,8 @@ export const DeleteCronResponse = {
}, },
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(base?: I): DeleteCronResponse { create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(base?: I): DeleteCronResponse {
return DeleteCronResponse.fromPartial(base ?? {}); return DeleteCronResponse.fromPartial(base ?? ({} as any));
}, },
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(_: I): DeleteCronResponse { fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(_: I): DeleteCronResponse {
const message = createBaseDeleteCronResponse(); const message = createBaseDeleteCronResponse();
return message; return message;
@@ -484,12 +496,14 @@ export interface CronClient extends Client {
export const CronClient = makeGenericClientConstructor(CronService, "com.ql.cron.Cron") as unknown as { export const CronClient = makeGenericClientConstructor(CronService, "com.ql.cron.Cron") as unknown as {
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): CronClient; new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): CronClient;
service: typeof CronService; service: typeof CronService;
serviceName: string;
}; };
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
export type DeepPartial<T> = T extends Builtin ? T export type DeepPartial<T> = T extends Builtin ? T
: T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>> : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> } : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>; : Partial<T>;
@@ -500,3 +514,12 @@ export type Exact<P, I extends P> = P extends Builtin ? P
function isSet(value: any): boolean { function isSet(value: any): boolean {
return value !== null && value !== undefined; return value !== null && value !== undefined;
} }
export interface MessageFns<T> {
encode(message: T, writer?: BinaryWriter): BinaryWriter;
decode(input: BinaryReader | Uint8Array, length?: number): T;
fromJSON(object: any): T;
toJSON(message: T): unknown;
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
}
+50 -29
View File
@@ -1,19 +1,25 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.6.1
// protoc v3.17.3
// source: back/protos/health.proto
/* eslint-disable */ /* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
import { import {
CallOptions, type CallOptions,
ChannelCredentials, ChannelCredentials,
Client, Client,
ClientOptions, type ClientOptions,
ClientReadableStream, type ClientReadableStream,
ClientUnaryCall, type ClientUnaryCall,
handleServerStreamingCall, type handleServerStreamingCall,
handleUnaryCall, type handleUnaryCall,
makeGenericClientConstructor, makeGenericClientConstructor,
Metadata, Metadata,
ServiceError, type ServiceError,
UntypedServiceImplementation, type UntypedServiceImplementation,
} from "@grpc/grpc-js"; } from "@grpc/grpc-js";
import _m0 from "protobufjs/minimal";
export const protobufPackage = "com.ql.health"; export const protobufPackage = "com.ql.health";
@@ -74,51 +80,53 @@ function createBaseHealthCheckRequest(): HealthCheckRequest {
return { service: "" }; return { service: "" };
} }
export const HealthCheckRequest = { export const HealthCheckRequest: MessageFns<HealthCheckRequest> = {
encode(message: HealthCheckRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(message: HealthCheckRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.service !== "") { if (message.service !== "") {
writer.uint32(10).string(message.service); writer.uint32(10).string(message.service);
} }
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckRequest { decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckRequest {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHealthCheckRequest(); const message = createBaseHealthCheckRequest();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: case 1: {
if (tag !== 10) { if (tag !== 10) {
break; break;
} }
message.service = reader.string(); message.service = reader.string();
continue; continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skip(tag & 7);
} }
return message; return message;
}, },
fromJSON(object: any): HealthCheckRequest { fromJSON(object: any): HealthCheckRequest {
return { service: isSet(object.service) ? String(object.service) : "" }; return { service: isSet(object.service) ? globalThis.String(object.service) : "" };
}, },
toJSON(message: HealthCheckRequest): unknown { toJSON(message: HealthCheckRequest): unknown {
const obj: any = {}; const obj: any = {};
message.service !== undefined && (obj.service = message.service); if (message.service !== "") {
obj.service = message.service;
}
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(base?: I): HealthCheckRequest { create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(base?: I): HealthCheckRequest {
return HealthCheckRequest.fromPartial(base ?? {}); return HealthCheckRequest.fromPartial(base ?? ({} as any));
}, },
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(object: I): HealthCheckRequest { fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(object: I): HealthCheckRequest {
const message = createBaseHealthCheckRequest(); const message = createBaseHealthCheckRequest();
message.service = object.service ?? ""; message.service = object.service ?? "";
@@ -130,33 +138,34 @@ function createBaseHealthCheckResponse(): HealthCheckResponse {
return { status: 0 }; return { status: 0 };
} }
export const HealthCheckResponse = { export const HealthCheckResponse: MessageFns<HealthCheckResponse> = {
encode(message: HealthCheckResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(message: HealthCheckResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.status !== 0) { if (message.status !== 0) {
writer.uint32(8).int32(message.status); writer.uint32(8).int32(message.status);
} }
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckResponse { decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckResponse {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHealthCheckResponse(); const message = createBaseHealthCheckResponse();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: case 1: {
if (tag !== 8) { if (tag !== 8) {
break; break;
} }
message.status = reader.int32() as any; message.status = reader.int32() as any;
continue; continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skip(tag & 7);
} }
return message; return message;
}, },
@@ -167,14 +176,15 @@ export const HealthCheckResponse = {
toJSON(message: HealthCheckResponse): unknown { toJSON(message: HealthCheckResponse): unknown {
const obj: any = {}; const obj: any = {};
message.status !== undefined && (obj.status = healthCheckResponse_ServingStatusToJSON(message.status)); if (message.status !== 0) {
obj.status = healthCheckResponse_ServingStatusToJSON(message.status);
}
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(base?: I): HealthCheckResponse { create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(base?: I): HealthCheckResponse {
return HealthCheckResponse.fromPartial(base ?? {}); return HealthCheckResponse.fromPartial(base ?? ({} as any));
}, },
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(object: I): HealthCheckResponse { fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(object: I): HealthCheckResponse {
const message = createBaseHealthCheckResponse(); const message = createBaseHealthCheckResponse();
message.status = object.status ?? 0; message.status = object.status ?? 0;
@@ -236,12 +246,14 @@ export interface HealthClient extends Client {
export const HealthClient = makeGenericClientConstructor(HealthService, "com.ql.health.Health") as unknown as { export const HealthClient = makeGenericClientConstructor(HealthService, "com.ql.health.Health") as unknown as {
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): HealthClient; new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): HealthClient;
service: typeof HealthService; service: typeof HealthService;
serviceName: string;
}; };
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
export type DeepPartial<T> = T extends Builtin ? T export type DeepPartial<T> = T extends Builtin ? T
: T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>> : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> } : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>; : Partial<T>;
@@ -252,3 +264,12 @@ export type Exact<P, I extends P> = P extends Builtin ? P
function isSet(value: any): boolean { function isSet(value: any): boolean {
return value !== null && value !== undefined; return value !== null && value !== undefined;
} }
export interface MessageFns<T> {
encode(message: T, writer?: BinaryWriter): BinaryWriter;
decode(input: BinaryReader | Uint8Array, length?: number): T;
fromJSON(object: any): T;
toJSON(message: T): unknown;
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
}
-35
View File
@@ -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);
});
+7 -7
View File
@@ -10,23 +10,23 @@ const addCron = (
callback: sendUnaryData<AddCronResponse>, callback: sendUnaryData<AddCronResponse>,
) => { ) => {
for (const item of call.request.crons) { for (const item of call.request.crons) {
const { id, schedule, command, extraSchedules, name } = item; const { id, schedule, command, extra_schedules, name } = item;
if (scheduleStacks.has(id)) { if (scheduleStacks.has(id)) {
scheduleStacks.get(id)?.forEach((x) => x.cancel()); scheduleStacks.get(id)?.forEach((x) => x.cancel());
} }
Logger.info( Logger.info(
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s', '[schedule][创建定时任务] 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
id, id,
name, name,
schedule, schedule,
command, command,
); );
if (extraSchedules?.length) { if (extra_schedules?.length) {
extraSchedules.forEach((x) => { extra_schedules.forEach((x) => {
Logger.info( Logger.info(
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s', '[schedule][创建定时任务] 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
id, id,
name, name,
x.schedule, x.schedule,
@@ -40,8 +40,8 @@ const addCron = (
Logger.info(`[schedule][准备运行任务] 命令: ${command}`); Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
runCron(command, item); runCron(command, item);
}), }),
...(extraSchedules?.length ...(extra_schedules?.length
? extraSchedules.map((x) => ? extra_schedules.map((x) =>
nodeSchedule.scheduleJob(id, x.schedule, async () => { nodeSchedule.scheduleJob(id, x.schedule, async () => {
Logger.info(`[schedule][准备运行任务] 命令: ${command}`); Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
runCron(command, item); runCron(command, item);
+325
View File
@@ -0,0 +1,325 @@
import 'reflect-metadata';
import { Container } from 'typedi';
import EnvService from '../services/env';
import { sendUnaryData, ServerUnaryCall } from '@grpc/grpc-js';
import {
CreateEnvRequest,
CronItem,
DeleteEnvsRequest,
DisableEnvsRequest,
EnableEnvsRequest,
EnvItem,
EnvResponse,
EnvsResponse,
GetEnvByIdRequest,
GetEnvsRequest,
MoveEnvRequest,
Response,
SystemNotifyRequest,
UpdateEnvNamesRequest,
UpdateEnvRequest,
} from '../protos/api';
import LoggerInstance from '../loaders/logger';
import pick from 'lodash/pick';
import SystemService from '../services/system';
import CronService from '../services/cron';
import {
CronDetailRequest,
CronDetailResponse,
CreateCronRequest,
UpdateCronRequest,
DeleteCronsRequest,
CronResponse,
} from '../protos/api';
import { NotificationInfo } from '../data/notify';
Container.set('logger', LoggerInstance);
export const getEnvs = async (
call: ServerUnaryCall<GetEnvsRequest, EnvsResponse>,
callback: sendUnaryData<EnvsResponse>,
) => {
try {
const envService = Container.get(EnvService);
const data = await envService.envs(call.request.searchValue);
callback(null, {
code: 200,
data: data.map((x) => ({ ...x, remarks: x.remarks || '' })),
});
} catch (e: any) {
callback(null, {
code: 500,
data: [],
message: e.message,
});
}
};
export const createEnv = async (
call: ServerUnaryCall<CreateEnvRequest, EnvsResponse>,
callback: sendUnaryData<EnvsResponse>,
) => {
try {
const envService = Container.get(EnvService);
const data = await envService.create(call.request.envs);
callback(null, { code: 200, data });
} catch (e: any) {
callback(e);
}
};
export const updateEnv = async (
call: ServerUnaryCall<UpdateEnvRequest, EnvResponse>,
callback: sendUnaryData<EnvResponse>,
) => {
try {
if (!call.request.env?.id) {
return callback(null, {
code: 400,
data: undefined,
message: 'id parameter is required',
});
}
const envService = Container.get(EnvService);
const data = await envService.update(
pick(call.request.env, ['id', 'name', 'value', 'remarks']) as EnvItem,
);
callback(null, { code: 200, data });
} catch (e: any) {
callback(e);
}
};
export const deleteEnvs = async (
call: ServerUnaryCall<DeleteEnvsRequest, Response>,
callback: sendUnaryData<Response>,
) => {
try {
if (!call.request.ids || call.request.ids.length === 0) {
return callback(null, {
code: 400,
message: 'ids parameter is required',
});
}
const envService = Container.get(EnvService);
await envService.remove(call.request.ids);
callback(null, { code: 200 });
} catch (e: any) {
callback(e);
}
};
export const moveEnv = async (
call: ServerUnaryCall<MoveEnvRequest, EnvResponse>,
callback: sendUnaryData<EnvResponse>,
) => {
try {
if (!call.request.id) {
return callback(null, {
code: 400,
data: undefined,
message: 'id parameter is required',
});
}
const envService = Container.get(EnvService);
const data = await envService.move(call.request.id, {
fromIndex: call.request.fromIndex,
toIndex: call.request.toIndex,
});
callback(null, { code: 200, data });
} catch (e: any) {
callback(e);
}
};
export const disableEnvs = async (
call: ServerUnaryCall<DisableEnvsRequest, Response>,
callback: sendUnaryData<Response>,
) => {
try {
if (!call.request.ids || call.request.ids.length === 0) {
return callback(null, {
code: 400,
message: 'ids parameter is required',
});
}
const envService = Container.get(EnvService);
await envService.disabled(call.request.ids);
callback(null, { code: 200 });
} catch (e: any) {
callback(e);
}
};
export const enableEnvs = async (
call: ServerUnaryCall<EnableEnvsRequest, Response>,
callback: sendUnaryData<Response>,
) => {
try {
if (!call.request.ids || call.request.ids.length === 0) {
return callback(null, {
code: 400,
message: 'ids parameter is required',
});
}
const envService = Container.get(EnvService);
await envService.enabled(call.request.ids);
callback(null, { code: 200 });
} catch (e: any) {
callback(e);
}
};
export const updateEnvNames = async (
call: ServerUnaryCall<UpdateEnvNamesRequest, Response>,
callback: sendUnaryData<Response>,
) => {
try {
if (!call.request.ids || call.request.ids.length === 0) {
return callback(null, {
code: 400,
message: 'ids parameter is required',
});
}
const envService = Container.get(EnvService);
await envService.updateNames({
ids: call.request.ids,
name: call.request.name,
});
callback(null, { code: 200 });
} catch (e: any) {
callback(e);
}
};
export const getEnvById = async (
call: ServerUnaryCall<GetEnvByIdRequest, EnvResponse>,
callback: sendUnaryData<EnvResponse>,
) => {
try {
if (!call.request.id) {
return callback(null, {
code: 400,
data: undefined,
message: 'id parameter is required',
});
}
const envService = Container.get(EnvService);
const data = await envService.getDb({ id: call.request.id });
callback(null, {
code: 200,
data: { ...data, remarks: data.remarks || '' },
});
} catch (e: any) {
callback(e);
}
};
export const systemNotify = async (
call: ServerUnaryCall<SystemNotifyRequest, Response>,
callback: sendUnaryData<Response>,
) => {
try {
const systemService = Container.get(SystemService);
const data = await systemService.notify({
title: call.request.title,
content: call.request.content,
notificationInfo: call.request.notificationInfo as unknown as NotificationInfo,
});
callback(null, data);
} catch (e: any) {
callback(e);
}
};
const normalizeCronData = (data: CronItem | null): CronItem | undefined => {
if (!data) return undefined;
return {
...data,
sub_id: data.sub_id ?? undefined,
extra_schedules: data.extra_schedules ?? undefined,
pid: data.pid ?? undefined,
task_before: data.task_before ?? undefined,
task_after: data.task_after ?? undefined,
};
};
export const getCronDetail = async (
call: ServerUnaryCall<CronDetailRequest, CronDetailResponse>,
callback: sendUnaryData<CronDetailResponse>,
) => {
try {
if (!call.request.log_path) {
return callback(null, {
code: 400,
data: undefined,
message: 'log_path is required',
});
}
const cronService = Container.get(CronService);
const data = (await cronService.find({
log_path: call.request.log_path,
})) as CronItem;
callback(null, { code: 200, data: normalizeCronData(data) });
} catch (e: any) {
callback(e);
}
};
export const createCron = async (
call: ServerUnaryCall<CreateCronRequest, CronResponse>,
callback: sendUnaryData<CronResponse>,
) => {
try {
const cronService = Container.get(CronService);
const data = (await cronService.create(call.request)) as CronItem;
callback(null, { code: 200, data: normalizeCronData(data) });
} catch (e: any) {
callback(e);
}
};
export const updateCron = async (
call: ServerUnaryCall<UpdateCronRequest, CronResponse>,
callback: sendUnaryData<CronResponse>,
) => {
try {
const cronService = Container.get(CronService);
const { id, ...fields } = call.request;
const updateRequest = {
id,
...Object.entries(fields).reduce((acc: any, [key, value]) => {
if (value !== undefined) {
acc[key] = value;
}
return acc;
}, {}),
} as UpdateCronRequest;
const data = (await cronService.update(updateRequest)) as CronItem;
callback(null, { code: 200, data: normalizeCronData(data) });
} catch (e: any) {
callback(e);
}
};
export const deleteCrons = async (
call: ServerUnaryCall<DeleteCronsRequest, Response>,
callback: sendUnaryData<Response>,
) => {
try {
const cronService = Container.get(CronService);
await cronService.remove(call.request.ids);
callback(null, { code: 200 });
} catch (e: any) {
callback(e);
}
};
+1 -1
View File
@@ -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 },
); );
+1 -1
View File
@@ -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());
-24
View File
@@ -1,24 +0,0 @@
import { Server, ServerCredentials } from '@grpc/grpc-js';
import { CronService } from '../protos/cron';
import { addCron } from './addCron';
import { delCron } from './delCron';
import { HealthService } from '../protos/health';
import { check } from './health';
import config from '../config';
import Logger from '../loaders/logger';
const server = new Server({ 'grpc.enable_http_proxy': 0 });
server.addService(HealthService, { check });
server.addService(CronService, { addCron, delCron });
server.bindAsync(
`0.0.0.0:${config.cronPort}`,
ServerCredentials.createInsecure(),
(err, port) => {
if (err) {
throw err;
}
Logger.debug(`✌️ 定时服务启动成功!`);
console.debug(`✌️ 定时服务启动成功!`);
process.send?.('ready');
},
);
+3 -3
View File
@@ -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 {
+115 -43
View File
@@ -11,6 +11,7 @@ import {
killTask, killTask,
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';
@@ -22,6 +23,7 @@ import dayjs from 'dayjs';
import pickBy from 'lodash/pickBy'; import pickBy from 'lodash/pickBy';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { writeFileWithLock } from '../shared/utils'; import { writeFileWithLock } from '../shared/utils';
import { ScheduleType } from '../interface/schedule';
@Service() @Service()
export default class CronService { export default class CronService {
@@ -35,22 +37,40 @@ export default class CronService {
return false; return false;
} }
private isOnceSchedule(schedule?: string) {
return schedule?.startsWith(ScheduleType.ONCE);
}
private isBootSchedule(schedule?: string) {
return schedule?.startsWith(ScheduleType.BOOT);
}
private isSpecialSchedule(schedule?: string) {
return this.isOnceSchedule(schedule) || this.isBootSchedule(schedule);
}
public async create(payload: Crontab): Promise<Crontab> { public async create(payload: Crontab): Promise<Crontab> {
const tab = new Crontab(payload); const tab = new Crontab(payload);
tab.saved = false; tab.saved = false;
const doc = await this.insert(tab); const doc = await this.insert(tab);
if (this.isNodeCron(doc)) {
if (isDemoEnv()) {
return doc;
}
if (this.isNodeCron(doc) && !this.isSpecialSchedule(doc.schedule)) {
await cronClient.addCron([ await cronClient.addCron([
{ {
name: doc.name || '', name: doc.name || '',
id: String(doc.id), id: String(doc.id),
schedule: doc.schedule!, schedule: doc.schedule!,
command: this.makeCommand(doc), command: this.makeCommand(doc),
extraSchedules: doc.extra_schedules || [], extra_schedules: doc.extra_schedules || [],
}, },
]); ]);
} }
await this.set_crontab();
await this.setCrontab();
return doc; return doc;
} }
@@ -58,29 +78,33 @@ export default class CronService {
return await CrontabModel.create(payload, { returning: true }); return await CrontabModel.create(payload, { returning: true });
} }
public async update(payload: Crontab): Promise<Crontab> { public async update(payload: Partial<Crontab>): Promise<Crontab> {
const doc = await this.getDb({ id: payload.id }); const doc = await this.getDb({ id: payload.id });
const tab = new Crontab({ ...doc, ...payload }); const tab = new Crontab({ ...doc, ...payload });
tab.saved = false; tab.saved = false;
const newDoc = await this.updateDb(tab); const newDoc = await this.updateDb(tab);
if (doc.isDisabled === 1) {
if (doc.isDisabled === 1 || isDemoEnv()) {
return newDoc; return newDoc;
} }
if (this.isNodeCron(doc)) { if (this.isNodeCron(doc)) {
await cronClient.delCron([String(doc.id)]); await cronClient.delCron([String(doc.id)]);
} }
if (this.isNodeCron(newDoc)) {
if (this.isNodeCron(newDoc) && !this.isSpecialSchedule(newDoc.schedule)) {
await cronClient.addCron([ await cronClient.addCron([
{ {
name: doc.name || '', name: doc.name || '',
id: String(newDoc.id), id: String(newDoc.id),
schedule: newDoc.schedule!, schedule: newDoc.schedule!,
command: this.makeCommand(newDoc), command: this.makeCommand(newDoc),
extraSchedules: newDoc.extra_schedules || [], extra_schedules: newDoc.extra_schedules || [],
}, },
]); ]);
} }
await this.set_crontab();
await this.setCrontab();
return newDoc; return newDoc;
} }
@@ -135,7 +159,7 @@ export default class CronService {
public async remove(ids: number[]) { public async remove(ids: number[]) {
await CrontabModel.destroy({ where: { id: ids } }); await CrontabModel.destroy({ where: { id: ids } });
await cronClient.delCron(ids.map(String)); await cronClient.delCron(ids.map(String));
await this.set_crontab(); await this.setCrontab();
} }
public async pin(ids: number[]) { public async pin(ids: number[]) {
@@ -179,8 +203,8 @@ export default class CronService {
for (const col of viewQuery.filters) { for (const col of viewQuery.filters) {
const { property, value, operation } = col; const { property, value, operation } = col;
let q: any = {}; let q: any = {};
let operate2 = null; let operate2: any = null;
let operate = null; let operate: any = null;
switch (operation) { switch (operation) {
case 'Reg': case 'Reg':
operate = Op.like; operate = Op.like;
@@ -191,21 +215,38 @@ 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] = [
{ {
[property]: { [Op.or]: [
[Op.notIn]: Array.isArray(value) ? value : [value], {
}, [property]: {
[Op.notIn]: Array.isArray(value) ? value : [value],
},
},
{
[property]: { [Op.is]: null },
},
],
}, },
property === 'status' && value.includes(2) property === 'status' && value.includes(2)
? { isDisabled: { [Op.ne]: 1 } } ? { isDisabled: { [Op.ne]: 1 } }
@@ -320,10 +361,10 @@ export default class CronService {
log_path, log_path,
}: { }: {
log_path: string; log_path: string;
}): Promise<Crontab | null> { }): Promise<Crontab | undefined> {
try { try {
const result = await CrontabModel.findOne({ where: { log_path } }); const result = await CrontabModel.findOne({ where: { log_path } });
return result; return result?.get({ plain: true });
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -374,7 +415,7 @@ export default class CronService {
try { try {
const result = await CrontabModel.findAll(condition); const result = await CrontabModel.findAll(condition);
const count = await CrontabModel.count({ where: query }); const count = await CrontabModel.count({ where: query });
return { data: result, total: count }; return { data: result.map((x) => x.get({ plain: true })), total: count };
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -424,7 +465,7 @@ export default class CronService {
name: cron.name, name: cron.name,
command: cron.command, command: cron.command,
schedule: cron.schedule, schedule: cron.schedule,
extraSchedules: cron.extra_schedules, extra_schedules: cron.extra_schedules,
}; };
if (cron.status !== CrontabStatus.queued) { if (cron.status !== CrontabStatus.queued) {
resolve(params); resolve(params);
@@ -495,23 +536,27 @@ export default class CronService {
public async disabled(ids: number[]) { public async disabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } }); await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
await cronClient.delCron(ids.map(String)); await cronClient.delCron(ids.map(String));
await this.set_crontab(); await this.setCrontab();
} }
public async enabled(ids: number[]) { public async enabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } }); await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
const docs = await CrontabModel.findAll({ where: { id: ids } }); const docs = await CrontabModel.findAll({ where: { id: ids } });
const sixCron = docs const sixCron = docs
.filter((x) => this.isNodeCron(x)) .filter((x) => this.isNodeCron(x) && !this.isSpecialSchedule(x.schedule))
.map((doc) => ({ .map((doc) => ({
name: doc.name || '', name: doc.name || '',
id: String(doc.id), id: String(doc.id),
schedule: doc.schedule!, schedule: doc.schedule!,
command: this.makeCommand(doc), command: this.makeCommand(doc),
extraSchedules: doc.extra_schedules || [], extra_schedules: doc.extra_schedules || [],
})); }));
if (isDemoEnv()) {
return;
}
await cronClient.addCron(sixCron); await cronClient.addCron(sixCron);
await this.set_crontab(); await this.setCrontab();
} }
public async log(id: number) { public async log(id: number) {
@@ -525,7 +570,10 @@ export default class CronService {
if (logFileExist) { if (logFileExist) {
return await getFileContentByName(`${absolutePath}`); return await getFileContentByName(`${absolutePath}`);
} else { } else {
return '任务未运行'; return typeof doc.status === 'number' &&
[CrontabStatus.queued, CrontabStatus.running].includes(doc.status)
? '运行中...'
: '日志不存在...';
} }
} }
@@ -545,7 +593,7 @@ export default class CronService {
files.map(async (x) => ({ files.map(async (x) => ({
filename: x, filename: x,
directory: relativeDir.replace(config.logPath, ''), directory: relativeDir.replace(config.logPath, ''),
time: (await fs.lstat(`${dir}/${x}`)).mtime.getTime(), time: (await fs.lstat(`${dir}/${x}`)).birthtimeMs,
})), })),
) )
).sort((a, b) => b.time - a.time); ).sort((a, b) => b.time - a.time);
@@ -579,15 +627,14 @@ export default class CronService {
return crontab_job_string; return crontab_job_string;
} }
private async set_crontab(data?: { data: Crontab[]; total: number }) { private async setCrontab(data?: { data: Crontab[]; total: number }) {
const tabs = data ?? (await this.crontabs()); const tabs = data ?? (await this.crontabs());
var crontab_string = ''; var crontab_string = '';
tabs.data.forEach((tab) => { tabs.data.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(/ +/);
if ( if (
tab.isDisabled === 1 || tab.isDisabled === 1 ||
_schedule!.length !== 5 || this.isNodeCron(tab) ||
tab.extra_schedules?.length this.isSpecialSchedule(tab.schedule)
) { ) {
crontab_string += '# '; crontab_string += '# ';
crontab_string += tab.schedule; crontab_string += tab.schedule;
@@ -608,7 +655,7 @@ export default class CronService {
await CrontabModel.update({ saved: true }, { where: {} }); await CrontabModel.update({ saved: true }, { where: {} });
} }
public import_crontab() { public importCrontab() {
exec('crontab -l', (error, stdout, stderr) => { exec('crontab -l', (error, stdout, stderr) => {
const lines = stdout.split('\n'); const lines = stdout.split('\n');
const namePrefix = new Date().getTime(); const namePrefix = new Date().getTime();
@@ -644,17 +691,42 @@ export default class CronService {
public async autosave_crontab() { public async autosave_crontab() {
const tabs = await this.crontabs(); const tabs = await this.crontabs();
this.set_crontab(tabs); const regularCrons = tabs.data
.filter(
const sixCron = tabs.data (x) =>
.filter((x) => this.isNodeCron(x) && x.isDisabled !== 1) x.isDisabled !== 1 &&
this.isNodeCron(x) &&
!this.isSpecialSchedule(x.schedule),
)
.map((doc) => ({ .map((doc) => ({
name: doc.name || '', name: doc.name || '',
id: String(doc.id), id: String(doc.id),
schedule: doc.schedule!, schedule: doc.schedule!,
command: this.makeCommand(doc), command: this.makeCommand(doc),
extraSchedules: doc.extra_schedules || [], extra_schedules: doc.extra_schedules || [],
})); }));
await cronClient.addCron(sixCron);
if (isDemoEnv()) {
await writeFileWithLock(config.crontabFile, '');
return;
}
await cronClient.addCron(regularCrons);
this.setCrontab(tabs);
}
public async bootTask() {
const tabs = await this.crontabs();
const bootTasks = tabs.data.filter(
(x) => !x.isDisabled && this.isBootSchedule(x.schedule),
);
if (bootTasks.length > 0) {
await CrontabModel.update(
{ status: CrontabStatus.queued },
{ where: { id: bootTasks.map((t) => t.id!) } },
);
for (const task of bootTasks) {
await this.runSingle(task.id!);
}
}
} }
} }
+36 -43
View File
@@ -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({
+7 -7
View File
@@ -41,7 +41,7 @@ export default class EnvService {
} }
public async insert(payloads: Env[]): Promise<Env[]> { public async insert(payloads: Env[]): Promise<Env[]> {
const result = []; const result: Env[] = [];
for (const env of payloads) { for (const env of payloads) {
const doc = await EnvModel.create(env, { returning: true }); const doc = await EnvModel.create(env, { returning: true });
result.push(doc); result.push(doc);
@@ -62,7 +62,7 @@ export default class EnvService {
return await this.getDb({ id: payload.id }); return await this.getDb({ id: payload.id });
} }
public async remove(ids: string[]) { public async remove(ids: number[]) {
await EnvModel.destroy({ where: { id: ids } }); await EnvModel.destroy({ where: { id: ids } });
await this.set_envs(); await this.set_envs();
} }
@@ -150,7 +150,7 @@ export default class EnvService {
['position', 'DESC'], ['position', 'DESC'],
['createdAt', 'ASC'], ['createdAt', 'ASC'],
]); ]);
return result as any; return result;
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -161,7 +161,7 @@ export default class EnvService {
where: { ...query }, where: { ...query },
order: [...sort], order: [...sort],
}); });
return docs; return docs.map((x) => x.get({ plain: true }));
} }
public async getDb(query: FindOptions<Env>['where']): Promise<Env> { public async getDb(query: FindOptions<Env>['where']): Promise<Env> {
@@ -172,7 +172,7 @@ export default class EnvService {
return doc.get({ plain: true }); return doc.get({ plain: true });
} }
public async disabled(ids: string[]) { public async disabled(ids: number[]) {
await EnvModel.update( await EnvModel.update(
{ status: EnvStatus.disabled }, { status: EnvStatus.disabled },
{ where: { id: ids } }, { where: { id: ids } },
@@ -180,12 +180,12 @@ export default class EnvService {
await this.set_envs(); await this.set_envs();
} }
public async enabled(ids: string[]) { public async enabled(ids: number[]) {
await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } }); await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } });
await this.set_envs(); await this.set_envs();
} }
public async updateNames({ ids, name }: { ids: string[]; name: string }) { public async updateNames({ ids, name }: { ids: number[]; name: string }) {
await EnvModel.update({ name }, { where: { id: ids } }); await EnvModel.update({ name }, { where: { id: ids } });
await this.set_envs(); await this.set_envs();
} }
+64
View File
@@ -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;
}
}
+72
View File
@@ -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;
}
}
+53
View File
@@ -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;
}
}
+14
View File
@@ -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 : '';
}
}
+92
View File
@@ -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();
+230 -239
View File
@@ -1,14 +1,11 @@
import crypto from 'crypto'; import crypto from 'crypto';
import got from 'got';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import winston from 'winston'; import { parseBody, parseHeaders } from '../config/util';
import { parseBody, parseHeaders, safeJSONParse } from '../config/util';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import UserService from './user'; import UserService from './user';
import { readFile } from 'fs/promises'; import { httpClient } from '../config/http';
import config from '../config'; import { ProxyAgent } from 'undici';
@Service() @Service()
export default class NotificationService { export default class NotificationService {
@@ -49,41 +46,29 @@ export default class NotificationService {
constructor() {} constructor() {}
public async externalNotify(
title: string,
content: string,
): Promise<boolean | undefined> {
const { type, ...rest } = safeJSONParse(
await readFile(config.systemNotifyFile, 'utf-8'),
);
if (type) {
this.title = title;
this.content = content;
this.params = rest;
const notificationModeAction = this.modeMap.get(type);
try {
return await notificationModeAction?.call(this);
} catch (error: any) {
throw error;
}
}
return false;
}
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;
@@ -108,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,
@@ -119,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 {
@@ -134,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 {
@@ -154,18 +138,19 @@ export default class NotificationService {
private async serverChan() { private async serverChan() {
const { serverChanKey } = this.params; const { serverChanKey } = this.params;
const matchResult = serverChanKey.match(/^sctp(\d+)t/i); const matchResult = serverChanKey.match(/^sctp(\d+)t/i);
const url = matchResult && matchResult[1] const url =
? `https://${matchResult[1]}.push.ft07.com/send/${serverChanKey}.send` matchResult && matchResult[1]
: `https://sctapi.ftqq.com/${serverChanKey}.send`; ? `https://${matchResult[1]}.push.ft07.com/send/${serverChanKey}.send`
: `https://sctapi.ftqq.com/${serverChanKey}.send`;
try { try {
const res: any = await got const res = await httpClient.post(url, {
.post(url, { ...this.gotOption,
...this.gotOption, body: `title=${encodeURIComponent(
body: `title=${encodeURIComponent(this.title)}&desp=${encodeURIComponent(this.content)}`, this.title,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, )}&desp=${encodeURIComponent(this.content)}`,
}) 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 {
@@ -180,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
@@ -203,16 +186,13 @@ export default class NotificationService {
} }
private async chat() { private async chat() {
const { chatUrl, chatToken } = this.params; const { synologyChatUrl } = this.params;
const url = `${chatUrl}${chatToken}`;
try { try {
const res: any = await got const res = await httpClient.post(synologyChatUrl, {
.post(url, { ...this.gotOption,
...this.gotOption, body: `payload={"text":"${this.title}\n${this.content}"}`,
body: `payload={"text":"${this.title}\n${this.content}"}`, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, });
})
.json();
if (res.success) { if (res.success) {
return true; return true;
} else { } else {
@@ -248,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 {
@@ -280,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 {
@@ -325,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 {
@@ -351,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 {
@@ -378,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',
@@ -427,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;
@@ -482,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 {
@@ -504,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;
@@ -520,10 +474,18 @@ export default class NotificationService {
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
} }
private async pushPlus() { private async pushPlus() {
const { pushPlusToken, pushPlusUser, pushplusWebhook, pushPlusTemplate, pushplusChannel, pushplusCallbackUrl, pushplusTo} = this.params; const {
pushPlusToken,
pushPlusUser,
pushplusWebhook,
pushPlusTemplate,
pushplusChannel,
pushplusCallbackUrl,
pushplusTo,
} = this.params;
const url = `https://www.pushplus.plus/send`; const url = `https://www.pushplus.plus/send`;
try { try {
let body = { let body = {
@@ -537,13 +499,11 @@ export default class NotificationService {
channel: `${pushplusChannel || 'wechat'}`, channel: `${pushplusChannel || 'wechat'}`,
webhook: `${pushplusWebhook || ''}`, webhook: `${pushplusWebhook || ''}`,
callbackUrl: `${pushplusCallbackUrl || ''}`, callbackUrl: `${pushplusCallbackUrl || ''}`,
to: `${pushplusTo || ''}` to: `${pushplusTo || ''}`,
}, },
} };
const res: any = await got const res = await httpClient.post(url, body);
.post(url, body)
.json();
if (res.code === 200) { if (res.code === 200) {
return true; return true;
@@ -567,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;
@@ -599,16 +557,14 @@ export default class NotificationService {
} }
try { try {
const res: any = await got const res = await httpClient.post(larkKey, {
.post(larkKey, { ...this.gotOption,
...this.gotOption, json: {
json: { msg_type: 'text',
msg_type: 'text', content: { text: `${this.title}\n\n${this.content}` },
content: { text: `${this.title}\n\n${this.content}` }, },
}, headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json' }, });
})
.json();
if (res.StatusCode === 0 || res.code === 0) { if (res.StatusCode === 0 || res.code === 0) {
return true; return true;
} else { } else {
@@ -620,7 +576,7 @@ export default class NotificationService {
} }
private async email() { private async email() {
const { emailPass, emailService, emailUser } = this.params; const { emailPass, emailService, emailUser, emailTo } = this.params;
try { try {
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
@@ -633,7 +589,7 @@ export default class NotificationService {
const info = await transporter.sendMail({ const info = await transporter.sendMail({
from: `"青龙快讯" <${emailUser}>`, from: `"青龙快讯" <${emailUser}>`,
to: `${emailUser}`, to: emailTo ? emailTo.split(';') : emailUser,
subject: `${this.title}`, subject: `${this.title}`,
html: `${this.content.replace(/\n/g, '<br/>')}`, html: `${this.content.replace(/\n/g, '<br/>')}`,
}); });
@@ -653,19 +609,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);
@@ -673,42 +632,74 @@ 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 Title: encodeRfc2047(this.title),
.post(`${ntfyUrl || 'https://ntfy.sh'}/${ntfyTopic}`, { Priority: `${ntfyPriority || '3'}`,
...this.gotOption, Icon: 'https://qn.whyour.cn/logo.png',
body: `${this.content}`, };
headers: { 'Title': encodedTitle, 'Priority': `${ntfyPriority || '3'}` }, if (ntfyToken) {
}); headers['Authorization'] = `Bearer ${ntfyToken}`;
if (res.statusCode === 200) { } else if (ntfyUsername && ntfyPassword) {
return true; headers['Authorization'] = `Basic ${Buffer.from(
} else { `${ntfyUsername}:${ntfyPassword}`,
throw new Error(JSON.stringify(res)); ).toString('base64')}`;
} }
if (ntfyActions) {
headers['Actions'] = encodeRfc2047(ntfyActions);
}
const res = await httpClient.request(
`${ntfyUrl || 'https://ntfy.sh'}/${ntfyTopic}`,
{
...this.gotOption,
body: `${this.content}`,
headers: headers,
method: 'POST',
},
);
if (res.statusCode === 200) {
return true;
} else {
throw new Error(await res.body.text());
}
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
} }
private async wxPusherBot() { private async wxPusherBot() {
const { wxPusherBotAppToken, wxPusherBotTopicIds, wxPusherBotUids } = this.params; const { wxPusherBotAppToken, wxPusherBotTopicIds, wxPusherBotUids } =
this.params;
// 处理 topicIds,将分号分隔的字符串转为数组 // 处理 topicIds,将分号分隔的字符串转为数组
const topicIds = wxPusherBotTopicIds ? wxPusherBotTopicIds.split(';') const topicIds = wxPusherBotTopicIds
.map(id => id.trim()) ? wxPusherBotTopicIds
.filter(id => id) .split(';')
.map(id => parseInt(id)) : []; .map((id) => id.trim())
.filter((id) => id)
.map((id) => parseInt(id))
: [];
// 处理 uids,将分号分隔的字符串转为数组 // 处理 uids,将分号分隔的字符串转为数组
const uids = wxPusherBotUids ? wxPusherBotUids.split(';') const uids = wxPusherBotUids
.map(uid => uid.trim()) ? wxPusherBotUids
.filter(uid => uid) : []; .split(';')
.map((uid) => uid.trim())
.filter((uid) => uid)
: [];
// topic_ids 和 uids 至少要有一个 // topic_ids 和 uids 至少要有一个
if (!topicIds.length && !uids.length) { if (!topicIds.length && !uids.length) {
@@ -717,20 +708,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;
@@ -742,7 +731,6 @@ export default class NotificationService {
} }
} }
private async chronocat() { private async chronocat() {
const { chronocatURL, chronocatQQ, chronocatToken } = this.params; const { chronocatURL, chronocatQQ, chronocatToken } = this.params;
try { try {
@@ -782,15 +770,16 @@ export default class NotificationService {
}, },
], ],
}; };
const res: any = await got.post(url, { const res = await httpClient.request(url, {
...this.gotOption, ...this.gotOption,
json: data, json: data,
headers, headers,
method: 'POST',
}); });
if (res.statusCode === 200) { if (res.statusCode === 200) {
return true; return true;
} else { } else {
throw new Error(res.body); throw new Error(await res.body.text());
} }
} }
} }
@@ -809,7 +798,7 @@ export default class NotificationService {
webhookContentType, webhookContentType,
} = this.params; } = this.params;
if (!webhookUrl.includes('$title') && !webhookBody.includes('$title')) { if (!webhookUrl?.includes('$title') && !webhookBody?.includes('$title')) {
throw new Error('Url 或者 Body 中必须包含 $title'); throw new Error('Url 或者 Body 中必须包含 $title');
} }
@@ -825,15 +814,17 @@ export default class NotificationService {
allowGetBody: true, allowGetBody: true,
...bodyParam, ...bodyParam,
}; };
try { try {
const formatUrl = webhookUrl const formatUrl = webhookUrl
?.replaceAll('$title', encodeURIComponent(this.title)) ?.replaceAll('$title', encodeURIComponent(this.title))
?.replaceAll('$content', encodeURIComponent(this.content)); ?.replaceAll('$content', encodeURIComponent(this.content));
const res = await got(formatUrl, options); const res = await httpClient.request(formatUrl, options);
const text = await res.body.text();
if (String(res.statusCode).startsWith('20')) { if (String(res.statusCode).startsWith('20')) {
return true; return true;
} else { } else {
throw new Error(JSON.stringify(res)); throw new Error(await res.body.text());
} }
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
+8 -9
View File
@@ -159,16 +159,15 @@ export default class OpenService {
value: string; value: string;
expiration: number; expiration: number;
}> { }> {
let systemApp = ( const apps = await shareStore.getApps();
await AppModel.findOne({ const systemApp = apps?.find((x) => x.name === 'system');
where: { name: 'system' },
})
)?.get({ plain: true });
if (!systemApp) { if (!systemApp) {
systemApp = await this.create({ throw new Error('system app not found');
name: 'system', }
scopes: ['crons', 'system'], const now = Math.round(Date.now() / 1000);
} as App); const currentToken = systemApp.tokens?.find((x) => x.expiration > now);
if (currentToken) {
return currentToken;
} }
const { data } = await this.authToken({ const { data } = await this.authToken({
client_id: systemApp.client_id, client_id: systemApp.client_id,
+4 -4
View File
@@ -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,
); );
+7 -2
View File
@@ -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 '';
} }
+6 -2
View File
@@ -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',
},
); );
} }
+1 -1
View File
@@ -384,7 +384,7 @@ export default class SubscriptionService {
files.map(async (x) => ({ files.map(async (x) => ({
filename: x, filename: x,
directory: relativeDir.replace(config.logPath, ''), directory: relativeDir.replace(config.logPath, ''),
time: (await fs.lstat(`${dir}/${x}`)).mtime.getTime(), time: (await fs.lstat(`${dir}/${x}`)).birthtimeMs,
})), })),
) )
).sort((a, b) => b.time - a.time); ).sort((a, b) => b.time - a.time);
+85 -16
View File
@@ -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,
@@ -16,6 +16,7 @@ import {
promiseExec, promiseExec,
readDirs, readDirs,
rmPath, rmPath,
setSystemTimezone,
} from '../config/util'; } from '../config/util';
import { import {
DependenceModel, DependenceModel,
@@ -46,11 +47,14 @@ 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 });
return doc; return {
...doc,
info: { ...doc.info, timezone: doc.info?.timezone || 'Asia/Shanghai' },
};
} }
private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> { private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> {
@@ -272,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;
@@ -353,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 {
@@ -367,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',
@@ -400,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) {
@@ -440,7 +481,7 @@ export default class SystemService {
const logs = result const logs = result
.reverse() .reverse()
.filter((x) => x.title.endsWith('.log')) .filter((x) => x.title.endsWith('.log'))
.filter((x) => x.mtime >= startTime && x.mtime <= endTime); .filter((x) => x.createTime >= startTime && x.createTime <= endTime);
res.set({ res.set({
'Content-Length': sum(logs.map((x) => x.size)), 'Content-Length': sum(logs.map((x) => x.size)),
@@ -471,4 +512,32 @@ export default class SystemService {
await rmPath(path.join(config.systemLogPath, log.title)); await rmPath(path.join(config.systemLogPath, log.title));
} }
} }
public async updateTimezone(info: SystemModelInfo) {
if (!info.timezone) {
info.timezone = 'Asia/Shanghai';
}
const oDoc = await this.getSystemConfig();
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
const success = await setSystemTimezone(info.timezone);
if (success) {
return { code: 200, data: info };
} else {
return { code: 400, message: '设置时区失败' };
}
}
public async cleanDependence(type: 'node' | 'python3') {
if (!type || !['node', 'python3'].includes(type)) {
return { code: 400, message: '参数错误' };
}
try {
const finalPath = path.join(config.dependenceCachePath, type);
await fs.promises.rm(finalPath, { recursive: true });
} catch (error) { }
return { code: 200 };
}
} }
+36 -19
View File
@@ -1,15 +1,7 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import { import { createRandomString } from '../config/util';
createFile,
createRandomString,
fileExist,
getNetIp,
getPlatform,
safeJSONParse,
} from '../config/util';
import config from '../config'; import config from '../config';
import * as fs from 'fs/promises';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default'; import { authenticator } from '@otplib/preset-default';
import { import {
@@ -29,6 +21,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()
@@ -101,9 +95,9 @@ 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',
}); });
@@ -139,7 +133,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, {
@@ -272,7 +273,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,
@@ -355,11 +365,18 @@ export default class UserService {
} }
public async resetAuthInfo(info: Partial<AuthInfo>) { public async resetAuthInfo(info: Partial<AuthInfo>) {
const { retries, twoFactorActivated } = 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,
}); twoFactorActivated,
password,
username,
},
(x) => !isNil(x),
);
await this.updateAuthInfo(authInfo, payload);
} }
} }
+33 -3
View File
@@ -11,6 +11,9 @@ import {
IScheduleFn, IScheduleFn,
TCron, TCron,
} from './interface'; } from './interface';
import config from '../config';
import { credentials } from '@grpc/grpc-js';
import { ApiClient } from '../protos/api';
class TaskLimit { class TaskLimit {
private dependenyLimit = new PQueue({ concurrency: 1 }); private dependenyLimit = new PQueue({ concurrency: 1 });
@@ -33,6 +36,11 @@ class TaskLimit {
private systemLimit = new PQueue({ private systemLimit = new PQueue({
concurrency: Math.max(os.cpus().length, 4), concurrency: Math.max(os.cpus().length, 4),
}); });
private client = new ApiClient(
`0.0.0.0:${config.grpcPort}`,
credentials.createInsecure(),
{ 'grpc.enable_http_proxy': 0 },
);
get cronLimitActiveCount() { get cronLimitActiveCount() {
return this.cronLimit.pending; return this.cronLimit.pending;
@@ -126,9 +134,18 @@ class TaskLimit {
if (result?.length > 5) { if (result?.length > 5) {
if (repeatTimes < 3) { if (repeatTimes < 3) {
this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1); this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1);
this.notificationService.externalNotify( this.client.systemNotify(
'任务重复运行', {
`任务:${cron.name},命令:${cron.command},定时:${cron.schedule},处于运行中的超过 5 个,请检查定时设置`, title: '任务重复运行',
content: `任务:${cron.name},命令:${cron.command},定时:${cron.schedule},处于运行中的超过 5 个,请检查定时设置`,
},
(err, res) => {
if (err) {
Logger.error(
`[schedule][任务重复运行] 通知失败 ${JSON.stringify(err)}`,
);
}
},
); );
} }
Logger.warn(`[schedule][任务重复运行] 参数 ${JSON.stringify(cron)}`); Logger.warn(`[schedule][任务重复运行] 参数 ${JSON.stringify(cron)}`);
@@ -172,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>,
+22 -7
View File
@@ -1,27 +1,42 @@
import { lock } from 'proper-lockfile'; import { lock } from 'proper-lockfile';
import { writeFile, open } from 'fs/promises'; import os from 'os';
import path from 'path';
import { writeFile, open, chmod } from 'fs/promises';
import { fileExist } from '../config/util'; import { fileExist } from '../config/util';
function getUniqueLockPath(filePath: string) {
const sanitizedPath = filePath
.replace(/[<>:"/\\|?*]/g, '_')
.replace(/^_/, '');
return path.join(os.tmpdir(), `${sanitizedPath}.ql_lock`);
}
export async function writeFileWithLock( export async function writeFileWithLock(
path: 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') {
options = { encoding: options }; options = { encoding: options };
} }
if (!(await fileExist(path))) { if (!(await fileExist(filePath))) {
const fileHandle = await open(path, 'w'); const fileHandle = await open(filePath, 'w');
fileHandle.close(); fileHandle.close();
} }
const release = await lock(path, { const lockfilePath = getUniqueLockPath(filePath);
const release = await lock(filePath, {
retries: { retries: {
retries: 10, retries: 10,
factor: 2, factor: 2,
minTimeout: 100, minTimeout: 100,
maxTimeout: 3000, maxTimeout: 3000,
}, },
lockfilePath,
}); });
await writeFile(path, content, { encoding: 'utf8', ...options }); await writeFile(filePath, content, { encoding: 'utf8', ...options });
if (options?.mode) {
await chmod(filePath, options.mode);
}
await release(); await release();
} }
-1
View File
@@ -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';
+8 -5
View File
@@ -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/**/*"], "include": ["./**/*"],
"exclude": ["node_modules"], "exclude": ["node_modules"]
"files": ["./back/index.d.ts"]
} }
+11
View File
@@ -0,0 +1,11 @@
/// <reference types="express" />
export {};
declare global {
namespace Express {
interface Request {
platform: 'desktop' | 'mobile';
}
}
}
-27
View File
@@ -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();
+40
View File
@@ -0,0 +1,40 @@
import { Joi } from 'celebrate';
import cron_parser from 'cron-parser';
import { ScheduleType } from '../interface/schedule';
const validateSchedule = (value: string, helpers: any) => {
if (
value.startsWith(ScheduleType.ONCE) ||
value.startsWith(ScheduleType.BOOT)
) {
return value;
}
try {
if (cron_parser.parseExpression(value).hasNext()) {
return value;
}
} catch (e) {
return helpers.error('any.invalid');
}
return helpers.error('any.invalid');
};
export const scheduleSchema = Joi.string()
.required()
.custom(validateSchedule)
.messages({
'any.invalid': '无效的定时规则',
'string.empty': '定时规则不能为空',
});
export const commonCronSchema = {
name: Joi.string().optional(),
command: Joi.string().required(),
schedule: scheduleSchema,
labels: Joi.array().optional(),
sub_id: Joi.number().optional().allow(null),
extra_schedules: Joi.array().optional().allow(null),
task_before: Joi.string().optional().allow('').allow(null),
task_after: Joi.string().optional().allow('').allow(null),
};
+18 -13
View File
@@ -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
View File
@@ -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"]
+13 -20
View File
@@ -4,41 +4,34 @@ dir_shell=/ql/shell
. $dir_shell/share.sh . $dir_shell/share.sh
. $dir_shell/env.sh . $dir_shell/env.sh
echo -e "======================1. 检测配置文件========================\n" log_with_style() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
printf "\n[%s] [%7s] %s\n" "${timestamp}" "${level}" "${message}"
}
log_with_style "INFO" "🚀 1. 检测配置文件..."
import_config "$@" import_config "$@"
make_dir /etc/nginx/conf.d
make_dir /run/nginx
init_nginx
fix_config fix_config
pm2 l &>/dev/null pm2 l &>/dev/null
echo -e "======================2. 安装依赖========================\n" log_with_style "INFO" "⚙️ 2. 启动 pm2 服务..."
patch_version
echo -e "======================3. 启动nginx========================\n"
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "nginx启动成功...\n"
echo -e "======================4. 启动pm2服务========================\n"
reload_update
reload_pm2 reload_pm2
if [[ $AutoStartBot == true ]]; then if [[ $AutoStartBot == true ]]; then
echo -e "======================5. 启动bot========================\n" log_with_style "INFO" "🤖 3. 启动 bot..."
nohup ql bot >$dir_log/bot.log 2>&1 & nohup ql bot >$dir_log/bot.log 2>&1 &
echo -e "bot后台启动中...\n"
fi fi
if [[ $EnableExtraShell == true ]]; then if [[ $EnableExtraShell == true ]]; then
echo -e "====================6. 执行自定义脚本========================\n" log_with_style "INFO" "🛠️ 4. 执行自定义脚本..."
nohup ql extra >$dir_log/extra.log 2>&1 & nohup ql extra >$dir_log/extra.log 2>&1 &
echo -e "自定义脚本后台执行中...\n"
fi fi
echo -e "############################################################\n" log_with_style "SUCCESS" "🎉 容器启动成功!"
echo -e "容器启动成功..."
echo -e "############################################################\n"
crond -f >/dev/null crond -f >/dev/null
-93
View File
@@ -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;
}
}
-45
View File
@@ -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
View File
@@ -1,14 +1,14 @@
module.exports = { module.exports = {
apps: [ apps: [
{ {
name: 'schedule', name: 'qinglong',
max_restarts: 10, max_restarts: 5,
kill_timeout: 15000, kill_timeout: 1000,
wait_ready: true, wait_ready: true,
listen_timeout: 10000, listen_timeout: 5000,
source_map_support: true, source_map_support: true,
time: true, time: true,
script: 'static/build/schedule/index.js', script: 'static/build/app.js',
env: { env: {
http_proxy: '', http_proxy: '',
https_proxy: '', https_proxy: '',
@@ -18,25 +18,5 @@ module.exports = {
ALL_PROXY: '', ALL_PROXY: '',
}, },
}, },
{
name: 'public',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
source_map_support: true,
time: true,
script: 'static/build/public.js',
},
{
name: 'panel',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
source_map_support: true,
time: true,
script: 'static/build/app.js',
},
], ],
}; };
+12 -2
View File
@@ -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"
}
} }
-13
View File
@@ -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',
},
],
};
+18 -25
View File
@@ -2,18 +2,12 @@
"private": true, "private": true,
"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", "gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true,snakeToCamel=false",
"public": "npm run build:back && node static/build/public.js",
"update": "npm run build:back && node static/build/update.js",
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"postinstall": "max setup 2>/dev/null || true", "postinstall": "max setup 2>/dev/null || true",
"test": "umi-test", "test": "umi-test",
@@ -60,9 +54,9 @@
} }
}, },
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.12.3", "@grpc/grpc-js": "^1.14.0",
"@grpc/proto-loader": "^0.8.0",
"@otplib/preset-default": "^12.0.1", "@otplib/preset-default": "^12.0.1",
"@sentry/node": "^8.42.0",
"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",
@@ -71,12 +65,11 @@
"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",
"express": "^4.21.1", "express": "^4.21.2",
"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",
@@ -84,15 +77,13 @@
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"nedb": "^1.8.0",
"node-schedule": "^2.1.0", "node-schedule": "^2.1.0",
"nodemailer": "^6.9.16", "nodemailer": "^6.9.16",
"p-queue-cjs": "7.3.4", "p-queue-cjs": "7.3.4",
"protobufjs": "^7.4.0", "@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 +95,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",
@@ -115,7 +108,6 @@
"@monaco-editor/react": "4.2.1", "@monaco-editor/react": "4.2.1",
"@react-hook/resize-observer": "^2.0.2", "@react-hook/resize-observer": "^2.0.2",
"react-router-dom": "6.26.1", "react-router-dom": "6.26.1",
"@sentry/react": "^8.42.0",
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/cross-spawn": "^6.0.2", "@types/cross-spawn": "^6.0.2",
@@ -126,7 +118,6 @@
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.185", "@types/lodash": "^4.14.185",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"@types/nedb": "^1.8.12",
"@types/node": "^17.0.21", "@types/node": "^17.0.21",
"@types/node-schedule": "^1.3.2", "@types/node-schedule": "^1.3.2",
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
@@ -140,13 +131,14 @@
"@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.3.36", "@umijs/max": "^4.4.4",
"@umijs/ssr-darkreader": "^4.9.45", "@umijs/ssr-darkreader": "^4.9.45",
"ahooks": "^3.7.8", "ahooks": "^3.7.8",
"ansi-to-react": "^6.1.6", "ansi-to-react": "^6.1.6",
"antd": "^4.24.8", "antd": "^4.24.16",
"antd-img-crop": "^4.23.0", "antd-img-crop": "^4.23.0",
"axios": "^1.4.0", "axios": "^1.4.0",
"compression-webpack-plugin": "9.2.0", "compression-webpack-plugin": "9.2.0",
@@ -173,11 +165,12 @@
"react-split-pane": "^0.1.92", "react-split-pane": "^0.1.92",
"sockjs-client": "^1.6.0", "sockjs-client": "^1.6.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"ts-proto": "^1.146.0", "ts-proto": "^2.6.1",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"typescript": "5.2.2", "typescript": "5.2.2",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"virtualizedtableforantd4": "1.3.0", "virtualizedtableforantd4": "1.3.0",
"yorkie": "^2.0.0" "@types/compression": "^1.7.2",
"@types/helmet": "^4.0.0"
} }
} }
+272 -1260
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -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/
+75 -33
View File
@@ -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, // 启用一言(随机句子)
@@ -84,7 +119,8 @@ const push_config = {
AIBOTK_NAME: '', // 智能微秘书 发送群名 或者好友昵称和type要对应好 AIBOTK_NAME: '', // 智能微秘书 发送群名 或者好友昵称和type要对应好
SMTP_SERVICE: '', // 邮箱服务名称,比如 126、163、Gmail、QQ 等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json SMTP_SERVICE: '', // 邮箱服务名称,比如 126、163、Gmail、QQ 等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json
SMTP_EMAIL: '', // SMTP 发件邮箱,通知将会由自己发给自己 SMTP_EMAIL: '', // SMTP 发件邮箱
SMTP_TO: '', // SMTP 收件邮箱,默认通知将会发给发件邮箱
SMTP_PASSWORD: '', // SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定 SMTP_PASSWORD: '', // SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
SMTP_NAME: '', // SMTP 收发件人姓名,可随意填写 SMTP_NAME: '', // SMTP 收发件人姓名,可随意填写
@@ -104,6 +140,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 +162,9 @@ for (const key in push_config) {
const $ = { const $ = {
post: (params, callback) => { post: (params, callback) => {
const { url, ...others } = params; const { url, ...others } = params;
got.post(url, others).then( httpClient.post(url, others).then(
(res) => { async (res) => {
let body = res.body; let body = await res.body.text();
try { try {
body = JSON.parse(body); body = JSON.parse(body);
} catch (error) {} } catch (error) {}
@@ -137,9 +177,9 @@ const $ = {
}, },
get: (params, callback) => { get: (params, callback) => {
const { url, ...others } = params; const { url, ...others } = params;
got.get(url, others).then( httpClient.get(url, others).then(
(res) => { async (res) => {
let body = res.body; let body = await res.body.text();
try { try {
body = JSON.parse(body); body = JSON.parse(body);
} catch (error) {} } catch (error) {}
@@ -155,8 +195,8 @@ const $ = {
async function one() { async function one() {
const url = 'https://v1.hitokoto.cn/'; const url = 'https://v1.hitokoto.cn/';
const res = await got.get(url); const res = await httpClient.request(url);
const body = JSON.parse(res.body); const body = await res.body.json();
return `${body.hitokoto} ----${body.from}`; return `${body.hitokoto} ----${body.from}`;
} }
@@ -428,7 +468,7 @@ function tgBotNotify(text, desp) {
TG_PROXY_AUTH, TG_PROXY_AUTH,
} = push_config; } = push_config;
if (TG_BOT_TOKEN && TG_USER_ID) { if (TG_BOT_TOKEN && TG_USER_ID) {
const options = { let options = {
url: `${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`, url: `${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`,
json: { json: {
chat_id: `${TG_USER_ID}`, chat_id: `${TG_USER_ID}`,
@@ -441,21 +481,11 @@ function tgBotNotify(text, desp) {
timeout, timeout,
}; };
if (TG_PROXY_HOST && TG_PROXY_PORT) { if (TG_PROXY_HOST && TG_PROXY_PORT) {
const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent'); let agent;
const options = { agent = new ProxyAgent({
keepAlive: true, uri: `http://${TG_PROXY_AUTH}${TG_PROXY_HOST}:${TG_PROXY_PORT}`,
keepAliveMsecs: 1000, });
maxSockets: 256, options.dispatcher = agent;
maxFreeSockets: 256,
proxy: `http://${TG_PROXY_AUTH}${TG_PROXY_HOST}:${TG_PROXY_PORT}`,
};
const httpAgent = new HttpProxyAgent(options);
const httpsAgent = new HttpsProxyAgent(options);
const agent = {
http: httpAgent,
https: httpsAgent,
};
Object.assign(options, { agent });
} }
$.post(options, (err, resp, data) => { $.post(options, (err, resp, data) => {
try { try {
@@ -993,7 +1023,8 @@ function fsBotNotify(text, desp) {
} }
async function smtpNotify(text, desp) { async function smtpNotify(text, desp) {
const { SMTP_EMAIL, SMTP_PASSWORD, SMTP_SERVICE, SMTP_NAME } = push_config; const { SMTP_EMAIL, SMTP_TO, SMTP_PASSWORD, SMTP_SERVICE, SMTP_NAME } =
push_config;
if (![SMTP_EMAIL, SMTP_PASSWORD].every(Boolean) || !SMTP_SERVICE) { if (![SMTP_EMAIL, SMTP_PASSWORD].every(Boolean) || !SMTP_SERVICE) {
return; return;
} }
@@ -1011,7 +1042,7 @@ async function smtpNotify(text, desp) {
const addr = SMTP_NAME ? `"${SMTP_NAME}" <${SMTP_EMAIL}>` : SMTP_EMAIL; const addr = SMTP_NAME ? `"${SMTP_NAME}" <${SMTP_EMAIL}>` : SMTP_EMAIL;
const info = await transporter.sendMail({ const info = await transporter.sendMail({
from: addr, from: addr,
to: addr, to: SMTP_TO ? SMTP_TO.split(';') : addr,
subject: text, subject: text,
html: `${desp.replace(/\n/g, '<br/>')}`, html: `${desp.replace(/\n/g, '<br/>')}`,
}); });
@@ -1207,17 +1238,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 +1262,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 +1270,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) {
+12 -1
View File
@@ -126,6 +126,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 至少配置一个才行
@@ -806,7 +810,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)
+6
View File
@@ -6,4 +6,10 @@
*/ */
console.log('test scripts'); console.log('test scripts');
QLAPI.notify('test scripts', 'test desc'); QLAPI.notify('test scripts', 'test desc');
QLAPI.getEnvs({ searchValue: 'dddd' }).then((x) => {
console.log('getEnvs', x);
});
QLAPI.systemNotify({ title: '123', content: '231' }).then((x) => {
console.log('systemNotify', x);
});
console.log('test desc'); console.log('test desc');
+6 -1
View File
@@ -4,6 +4,11 @@ name: script name
定时规则 定时规则
cron: 1 9 * * * cron: 1 9 * * *
""" """
print("test script") print("test script")
QLAPI.notify('test script', 'test desc') print(QLAPI.notify("test script", "test desc"))
print("test systemNotify")
print(QLAPI.systemNotify({"title": "test script", "content": "dddd"}))
print("test getEnvs")
print(QLAPI.getEnvs({"searchValue": "1"}))
print("test desc") print("test desc")
+12 -49
View File
@@ -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:5700/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:5700/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:5700/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:5700/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,22 +143,20 @@ 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:5700/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
) )
code=$(echo "$api" | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code != 200 ]]; then if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n" if [[ ! $message ]]; then
message="$api"
fi
echo -e "${message}"
fi fi
} }
@@ -187,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:5700/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
) )
@@ -212,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:5700/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)
@@ -236,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:5700/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
) )
+11 -22
View File
@@ -20,29 +20,21 @@ 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" if [[ -f "$panelOut" ]]; then
tail -n 300 "$panelError" tail -n 300 "$panelOut"
}
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 else
echo -e "\n=====> nginx服务异常,重新启动nginx\n" echo "日志文件不存在: $panelOut"
nginx -c /etc/nginx/nginx.conf fi
if [[ -f "$panelError" ]]; then
tail -n 300 "$panelError"
else
echo "日志文件不存在: $panelError"
fi fi
} }
@@ -58,7 +50,7 @@ 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:5700/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:5700/crontab' \
@@ -74,14 +66,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"
} }
+140
View File
@@ -0,0 +1,140 @@
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const { join } = require('path');
class GrpcClient {
static #config = {
protoPath: join(process.env.QL_DIR, 'back/protos/api.proto'),
serverAddress: '0.0.0.0:5500',
protoOptions: {
keepCase: true,
longs: String,
enums: String,
defaults: true,
},
grpcOptions: {
'grpc.enable_http_proxy': 0,
},
defaultTimeout: 30000,
};
static #methods = [
'getEnvs',
'createEnv',
'updateEnv',
'deleteEnvs',
'moveEnv',
'disableEnvs',
'enableEnvs',
'updateEnvNames',
'getEnvById',
'systemNotify',
'getCronDetail',
'createCron',
'updateCron',
'deleteCrons',
];
#client;
#api = {};
constructor() {
this.#initializeClient();
this.#bindMethods();
}
#initializeClient() {
try {
const { protoPath, protoOptions, serverAddress, grpcOptions } =
GrpcClient.#config;
const packageDefinition = protoLoader.loadSync(protoPath, protoOptions);
const apiProto = grpc.loadPackageDefinition(packageDefinition).com.ql.api;
this.#client = new apiProto.Api(
serverAddress,
grpc.credentials.createInsecure(),
grpcOptions,
);
} catch (error) {
console.error('Failed to initialize gRPC client:', error);
process.exit(1);
}
}
#promisifyMethod(methodName) {
const capitalizedMethod =
methodName.charAt(0).toUpperCase() + methodName.slice(1);
const method = this.#client[capitalizedMethod].bind(this.#client);
return async (params = {}) => {
return new Promise((resolve, reject) => {
const metadata = new grpc.Metadata();
const deadline = new Date(
Date.now() + GrpcClient.#config.defaultTimeout,
);
method(params, metadata, { deadline }, (error, response) => {
if (error) {
return reject(error);
}
resolve(response);
});
});
};
}
#bindMethods() {
GrpcClient.#methods.forEach((method) => {
this.#api[method] = this.#promisifyMethod(method);
});
}
getApi() {
return {
...this.#api,
close: this.close.bind(this),
};
}
close() {
if (this.#client) {
this.#client.close();
this.#client = null;
}
}
}
const grpcClient = new GrpcClient();
process.on('SIGTERM', () => {
grpcClient.close();
process.exit(0);
});
process.on('SIGINT', () => {
grpcClient.close();
process.exit(0);
});
process.on('unhandledRejection', (reason, promise) => {
if (reason instanceof Error) {
if (reason.stack) {
const relevantStack = reason.stack
.split('\n')
.filter((line) => {
return (
!line.includes('node:internal') &&
!line.includes('node_modules/@grpc') &&
!line.includes('processTicksAndRejections')
);
})
.join('\n');
console.error(relevantStack);
}
} else {
console.error(reason);
}
});
module.exports = grpcClient.getApi();
+283
View File
@@ -0,0 +1,283 @@
import subprocess
import json
import tempfile
import os
from typing import Dict, List, TypedDict, Optional
from functools import wraps
def error_handler(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except TypeError as e:
if "missing" in str(e):
func_name = func.__name__
annotations = func.__annotations__
param_type = next(
(t for name, t in annotations.items() if name != "return"), None
)
if param_type and hasattr(param_type, "__annotations__"):
required_fields = {
k: v
for k, v in param_type.__annotations__.items()
if not getattr(param_type, "__total__", True)
or k in getattr(param_type, "__required_keys__", set())
}
fields_str = ", ".join(
f'"{k}": {v.__name__}' for k, v in required_fields.items()
)
raise Exception(
f"{func_name}() requires a dictionary with parameters: {{{fields_str}}}"
) from None
raise Exception(f"{str(e)}") from None
except Exception as e:
error_msg = str(e)
if "Error:" in error_msg:
error_msg = error_msg.split("Error:")[-1].split("\n")[0].strip()
raise Exception(f"{error_msg}") from None
return wrapper
class EnvItem(TypedDict, total=False):
id: Optional[int]
name: Optional[str]
value: Optional[str]
remarks: Optional[str]
status: Optional[int]
position: Optional[int]
class GetEnvsParams(TypedDict, total=False):
searchValue: str
class CreateEnvParams(TypedDict):
envs: List[EnvItem]
class UpdateEnvParams(TypedDict):
env: EnvItem
class DeleteEnvsParams(TypedDict):
ids: List[int]
class MoveEnvParams(TypedDict):
id: int
fromIndex: int
toIndex: int
class DisableEnvsParams(TypedDict):
ids: List[int]
class EnableEnvsParams(TypedDict):
ids: List[int]
class UpdateEnvNamesParams(TypedDict):
ids: List[int]
name: str
class GetEnvByIdParams(TypedDict):
id: int
class SystemNotifyParams(TypedDict):
title: str
content: str
class EnvsResponse(TypedDict):
code: int
data: List[EnvItem]
message: Optional[str]
class EnvResponse(TypedDict):
code: int
data: EnvItem
message: Optional[str]
class Response(TypedDict):
code: int
message: Optional[str]
class ExtraScheduleItem(TypedDict, total=False):
schedule: Optional[str]
class CronItem(TypedDict, total=False):
id: Optional[int]
command: Optional[str]
schedule: Optional[str]
name: Optional[str]
labels: List[str]
sub_id: Optional[int]
extra_schedules: List[ExtraScheduleItem]
task_before: Optional[str]
task_after: Optional[str]
status: Optional[int]
log_path: Optional[str]
pid: Optional[int]
last_running_time: Optional[int]
last_execution_time: Optional[int]
class CreateCronParams(TypedDict):
command: str
schedule: str
name: Optional[str]
labels: List[str]
sub_id: Optional[int]
extra_schedules: List[ExtraScheduleItem]
task_before: Optional[str]
task_after: Optional[str]
class UpdateCronParams(TypedDict):
id: int
command: str
schedule: str
name: Optional[str]
labels: List[str]
sub_id: Optional[int]
extra_schedules: List[ExtraScheduleItem]
task_before: Optional[str]
task_after: Optional[str]
class DeleteCronsParams(TypedDict):
ids: List[int]
class CronDetailParams(TypedDict):
log_path: str
class CronsResponse(TypedDict):
code: int
data: List[CronItem]
message: Optional[str]
class CronResponse(TypedDict):
code: int
data: CronItem
message: Optional[str]
class Client:
def __init__(self):
self.temp_dir = tempfile.mkdtemp(prefix="node_client_")
self.temp_script = os.path.join(self.temp_dir, "temp_script.js")
def __del__(self):
try:
if os.path.exists(self.temp_script):
os.remove(self.temp_script)
os.rmdir(self.temp_dir)
except Exception:
pass
@error_handler
def _execute_node(self, method: str, params: Dict = None) -> Dict:
node_code = f"""
const api = require('{os.getenv("QL_DIR")}/shell/preload/client.js');
(async () => {{
try {{
const result = await api.{method}({json.dumps(params) if params else ''});
console.log(JSON.stringify(result));
}} catch (error) {{
console.error(JSON.stringify({{
error: error.message,
stack: error.stack,
name: error.name
}}));
process.exit(1);
}}
}})();
"""
with open(self.temp_script, "w", encoding="utf-8") as f:
f.write(node_code)
result = subprocess.run(
["node", self.temp_script],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode != 0:
error_data = json.loads(result.stderr)
raise Exception(
f"{error_data.get('name', 'Error')}: {error_data.get('stack')}"
)
return json.loads(result.stdout)
@error_handler
def getEnvs(self, params: GetEnvsParams = None) -> EnvsResponse:
return self._execute_node("getEnvs", params)
@error_handler
def createEnv(self, data: CreateEnvParams) -> EnvsResponse:
return self._execute_node("createEnv", data)
@error_handler
def updateEnv(self, data: UpdateEnvParams) -> EnvResponse:
return self._execute_node("updateEnv", data)
@error_handler
def deleteEnvs(self, data: DeleteEnvsParams) -> Response:
return self._execute_node("deleteEnvs", data)
@error_handler
def moveEnv(self, data: MoveEnvParams) -> EnvResponse:
return self._execute_node("moveEnv", data)
@error_handler
def disableEnvs(self, data: DisableEnvsParams) -> Response:
return self._execute_node("disableEnvs", data)
@error_handler
def enableEnvs(self, data: EnableEnvsParams) -> Response:
return self._execute_node("enableEnvs", data)
@error_handler
def updateEnvNames(self, data: UpdateEnvNamesParams) -> Response:
return self._execute_node("updateEnvNames", data)
@error_handler
def getEnvById(self, data: GetEnvByIdParams) -> EnvResponse:
return self._execute_node("getEnvById", data)
@error_handler
def systemNotify(self, data: SystemNotifyParams) -> Response:
return self._execute_node("systemNotify", data)
@error_handler
def getCronDetail(self, data: CronDetailParams) -> CronResponse:
return self._execute_node("getCronDetail", data)
@error_handler
def createCron(self, data: CreateCronParams) -> CronResponse:
return self._execute_node("createCron", data)
@error_handler
def updateCron(self, data: UpdateCronParams) -> CronResponse:
return self._execute_node("updateCron", data)
@error_handler
def deleteCrons(self, data: DeleteCronsParams) -> Response:
return self._execute_node("deleteCrons", data)
+43 -20
View File
@@ -1,4 +1,5 @@
const { execSync } = require('child_process'); const { execSync } = require('child_process');
const client = require('./client.js');
require(`./env.js`); require(`./env.js`);
function expandRange(rangeStr, max) { function expandRange(rangeStr, max) {
@@ -37,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);
} }
@@ -63,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');
@@ -97,9 +119,10 @@ try {
run(); run();
const { sendNotify } = require('./notify.js'); const { sendNotify } = require('./__ql_notify__.js');
global.QLAPI = { global.QLAPI = {
notify: sendNotify, notify: sendNotify,
...client,
}; };
} catch (error) { } catch (error) {
console.log(`run builtin code error: `, error, '\n'); console.log(`run builtin code error: `, error, '\n');
+46 -23
View File
@@ -6,6 +6,7 @@ import builtins
import sys import sys
import env import env
import signal import signal
from client import Client
def try_parse_int(value): def try_parse_int(value):
@@ -42,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}'
task_before = os.getenv("task_before")
# 创建临时文件路径
temp_file = f"/tmp/env_{os.getpid()}.json"
# 构建命令数组
commands = [
f'source {os.getenv("file_task_before")} {file_name}'
]
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
@@ -106,9 +129,9 @@ try:
run() run()
from notify import send from __ql_notify__ import send
class BaseApi: class BaseApi(Client):
def notify(self, *args, **kwargs): def notify(self, *args, **kwargs):
return send(*args, **kwargs) return send(*args, **kwargs)
+3 -7
View File
@@ -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
+38 -123
View File
@@ -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
@@ -72,7 +70,17 @@ original_name=(
) )
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
} }
@@ -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
} }
@@ -295,14 +279,7 @@ reload_pm2() {
cd $dir_root cd $dir_root
restore_env_vars restore_env_vars
pm2 flush &>/dev/null pm2 flush &>/dev/null
pm2 startOrGracefulReload ecosystem.config.js pm2 startOrGracefulReload ecosystem.config.js --update-env
}
reload_update() {
cd $dir_root
restore_env_vars
pm2 flush &>/dev/null
pm2 startOrGracefulReload other.config.js
} }
diff_time() { diff_time() {
@@ -351,79 +328,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
@@ -438,8 +342,14 @@ clear_env() {
} }
handle_task_start() { handle_task_start() {
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp" local error_message=""
echo -e "## 开始执行... $begin_time\n" if [[ $ID ]]; then
local error=$(update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp")
if [[ $error ]]; then
error_message=", 任务状态更新失败(${error})"
fi
fi
echo -e "## 开始执行... ${begin_time}${error_message}\n"
} }
run_task_before() { run_task_before() {
@@ -468,12 +378,17 @@ handle_task_end() {
local end_timestamp=$(format_timestamp "$time_format" "$etime") local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp)) local diff_time=$(($end_timestamp - $begin_timestamp))
local suffix="" local suffix=""
[[ "$MANUAL" == "true" ]] && suffix="(手动停止)" [[ "${MANUAL:=}" == "true" ]] && suffix="(手动停止)"
[[ "$diff_time" == 0 ]] && diff_time=1 [[ "$diff_time" == 0 ]] && diff_time=1
echo -e "\n## 执行结束$suffix... $end_time 耗时 $diff_time 秒     " if [[ $ID ]]; then
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" local error=$(update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time")
if [[ $error ]]; then
error_message=", 任务状态更新失败(${error})"
fi
fi
echo -e "\n## 执行结束$suffix... $end_time 耗时 $diff_time${error_message:=}     "
} }
init_env init_env
+27 -22
View File
@@ -212,20 +212,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 +250,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 +313,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"
@@ -489,7 +487,6 @@ main() {
local time_format="%Y-%m-%d %H:%M:%S" local time_format="%Y-%m-%d %H:%M:%S"
local time=$(date "+$time_format") local time=$(date "+$time_format")
local begin_timestamp=$(format_timestamp "$time_format" "$time") local begin_timestamp=$(format_timestamp "$time_format" "$time")
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
local begin_time=$(format_time "$time_format" "$time") local begin_time=$(format_time "$time_format" "$time")
@@ -497,6 +494,8 @@ main() {
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
fi fi
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
case $p1 in case $p1 in
update) update)
fix_config fix_config
@@ -542,6 +541,12 @@ main() {
resettfa) resettfa)
eval update_auth_config "\\\"twoFactorActivated\\\":false" "禁用两步验证" $cmd eval update_auth_config "\\\"twoFactorActivated\\\":false" "禁用两步验证" $cmd
;; ;;
resetpwd)
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
@@ -552,7 +557,7 @@ main() {
local end_time=$(format_time "$time_format" "$etime") local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime") local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp)) local diff_time=$(($end_timestamp - $begin_timestamp))
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" [[ $ID ]] && update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time"
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒     " $cmd eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒     " $cmd
+33 -42
View File
@@ -1,35 +1,21 @@
import intl from 'react-intl-universal'; import config from '@/utils/config';
import React, { useEffect, useState } from 'react'; import { useCtx, useTheme } from '@/utils/hooks';
import ProLayout, { PageLoading } from '@ant-design/pro-layout'; import { request } from '@/utils/http';
import * as DarkReader from '@umijs/ssr-darkreader';
import defaultProps from './defaultProps';
import { Link, history, Outlet, useLocation } from '@umijs/max';
import { import {
LogoutOutlined, LogoutOutlined,
MenuFoldOutlined, MenuFoldOutlined,
MenuUnfoldOutlined, MenuUnfoldOutlined,
UserOutlined, UserOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import config from '@/utils/config'; import ProLayout, { PageLoading } from '@ant-design/pro-layout';
import { request } from '@/utils/http'; import { history, Link, Outlet, useLocation } from '@umijs/max';
import './index.less'; import * as DarkReader from '@umijs/ssr-darkreader';
import { Avatar, Badge, Dropdown, Image, MenuProps, Tooltip } from 'antd';
import React, { useEffect, useState } from 'react';
import intl from 'react-intl-universal';
import vhCheck from 'vh-check'; import vhCheck from 'vh-check';
import { useCtx, useTheme } from '@/utils/hooks'; import defaultProps from './defaultProps';
import { import './index.less';
message,
Badge,
Modal,
Avatar,
Dropdown,
Menu,
Image,
Popover,
Descriptions,
Tooltip,
MenuProps,
} from 'antd';
// @ts-ignore
import * as Sentry from '@sentry/react';
import { init } from '../utils/init'; import { init } from '../utils/init';
import WebSocketManager from '../utils/websocket'; import WebSocketManager from '../utils/websocket';
@@ -115,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));
}; };
@@ -178,9 +169,9 @@ export default function () {
useEffect(() => { useEffect(() => {
if (!user || !user.username) return; if (!user || !user.username) return;
const ws = WebSocketManager.getInstance( const ws = WebSocketManager.getInstance(
`${window.location.origin}${config.apiPrefix}ws?token=${localStorage.getItem( `${window.location.origin}${
config.authKey, config.apiPrefix
)}`, }ws?token=${localStorage.getItem(config.authKey)}`,
); );
return () => { return () => {
@@ -201,9 +192,6 @@ export default function () {
console.log( console.log(
`从开始至load总耗时: ${timing.loadEventEnd - timing.navigationStart}`, `从开始至load总耗时: ${timing.loadEventEnd - timing.navigationStart}`,
); );
Sentry.captureMessage(
`白屏时间 ${timing.responseStart - timing.navigationStart}`,
);
}; };
}, []); }, []);
@@ -254,18 +242,15 @@ export default function () {
<ProLayout <ProLayout
selectedKeys={[location.pathname]} selectedKeys={[location.pathname]}
loading={loading} loading={loading}
ErrorBoundary={Sentry.ErrorBoundary}
logo={ logo={
<> <>
<Image preview={false} src="https://qn.whyour.cn/logo.png" /> <Image preview={false} src="https://qn.whyour.cn/logo.png" />
<div className="title"> <div className="title">
<span className="title">{intl.get('青龙')}</span> <span className="title">{intl.get('青龙')}</span>
<a <span
href={systemInfo?.changeLogLink}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
window.open(systemInfo?.changeLogLink, '_blank');
}} }}
> >
<Tooltip <Tooltip
@@ -289,7 +274,7 @@ export default function () {
</span> </span>
</Badge> </Badge>
</Tooltip> </Tooltip>
</a> </span>
</div> </div>
</> </>
} }
@@ -320,7 +305,9 @@ export default function () {
shape="square" shape="square"
size="small" size="small"
icon={<UserOutlined />} icon={<UserOutlined />}
src={user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''} src={
user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''
}
/> />
<span style={{ marginLeft: 5 }}>{user.username}</span> <span style={{ marginLeft: 5 }}>{user.username}</span>
</span> </span>
@@ -342,7 +329,11 @@ export default function () {
shape="square" shape="square"
size="small" size="small"
icon={<UserOutlined />} icon={<UserOutlined />}
src={user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''} src={
user.avatar
? `${config.apiPrefix}static/${user.avatar}`
: ''
}
/> />
<span style={{ marginLeft: 5 }}>{user.username}</span> <span style={{ marginLeft: 5 }}>{user.username}</span>
</span> </span>
+29 -4
View File
@@ -186,6 +186,7 @@
"秒后重试": "Retry after seconds", "秒后重试": "Retry after seconds",
"在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。": "Open the two-factor authentication application on your device to view your authentication code and verify your identity.", "在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。": "Open the two-factor authentication application on your device to view your authentication code and verify your identity.",
"请选择脚本文件": "Please select a script file", "请选择脚本文件": "Please select a script file",
"当前文件不支持预览": "Current file type is not supported for preview",
"清空日志": "Clear Logs", "清空日志": "Clear Logs",
"设置": "Settings", "设置": "Settings",
"退出": "Exit", "退出": "Exit",
@@ -344,7 +345,7 @@
"gotify的url地址,例如 https://push.example.de:8080": "gotify URL address, e.g., https://push.example.de:8080", "gotify的url地址,例如 https://push.example.de:8080": "gotify URL address, e.g., https://push.example.de:8080",
"gotify的消息应用token码": "gotify message application token code", "gotify的消息应用token码": "gotify message application token code",
"推送消息的优先级": "Priority of Push Messages", "推送消息的优先级": "Priority of Push Messages",
"chat的url地址": "Chat URL address", "synologyChat的url地址": "Synology Chat Webhook URL address",
"chat的token码": "Chat token code", "chat的token码": "Chat token code",
"推送到个人QQ: http://127.0.0.1/send_private_msg,群:http://127.0.0.1/send_group_msg": "Push to personal QQ: http://127.0.0.1/send_private_msg, group: http://127.0.0.1/send_group_msg", "推送到个人QQ: http://127.0.0.1/send_private_msg,群:http://127.0.0.1/send_group_msg": "Push to personal QQ: http://127.0.0.1/send_private_msg, group: http://127.0.0.1/send_group_msg",
"访问密钥": "Access Key", "访问密钥": "Access Key",
@@ -393,8 +394,12 @@
"SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定": "The SMTP login password may also be a special passphrase, depending on the specific email service provider's instructions", "SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定": "The SMTP login password may also be a special passphrase, depending on the specific email service provider's instructions",
"PushMe的Keyhttps://push.i-i.me/": "PushMe key, https://push.i-i.me/", "PushMe的Keyhttps://push.i-i.me/": "PushMe key, https://push.i-i.me/",
"自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口": "The self built PushMeServer message interface address, for example: http://127.0.0.1:3010 If left blank, use the official message interface", "自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口": "The self built PushMeServer message interface address, for example: http://127.0.0.1:3010 If left blank, use the official message interface",
"ntfy的url地址,例如 https://ntfy.sh'": "The URL address of ntfy, for example, https://ntfy.sh.", "ntfy的url地址,例如 https://ntfy.sh": "The URL address of ntfy, for example, https://ntfy.sh.",
"ntfy的消息应用topic": "The topic for ntfy's messaging application.", "ntfy应用topic": "The topic for ntfy's application.",
"ntfy应用token": "The token for ntfy's application, see https://docs.ntfy.sh/config/#access-tokens",
"ntfy应用用户名": "The username for ntfy's application, see https://docs.ntfy.sh/config/#users-and-roles",
"ntfy应用密码": "The password for ntfy's application, see https://docs.ntfy.sh/config/#users-and-roles",
"ntfy用户动作": "The user actions for ntfy's application, up to three actions, see https://docs.ntfy.sh/publish/?h=actions#action-buttons",
"wxPusherBot的appToken": "wxPusherBot's appToken, obtain according to docs https://wxpusher.zjiecode.com/docs/", "wxPusherBot的appToken": "wxPusherBot's appToken, obtain according to docs https://wxpusher.zjiecode.com/docs/",
"wxPusherBot的topicIds": "wxPusherBot's topicIds, at least one of topicIds or uids must be configured", "wxPusherBot的topicIds": "wxPusherBot's topicIds, at least one of topicIds or uids must be configured",
"wxPusherBot的uids": "wxPusherBot's uids, at least one of topicIds or uids must be configured", "wxPusherBot的uids": "wxPusherBot's uids, at least one of topicIds or uids must be configured",
@@ -496,5 +501,25 @@
"NPM 镜像源": "NPM Mirror Source", "NPM 镜像源": "NPM Mirror Source",
"PyPI 镜像源": "PyPI Mirror Source", "PyPI 镜像源": "PyPI Mirror Source",
"alpine linux 镜像源": "Alpine Linux Mirror Source", "alpine linux 镜像源": "Alpine Linux Mirror Source",
"如果恢复失败,可进入容器执行": "If recovery fails, you can enter the container and execute" "如果恢复失败,可进入容器执行": "If recovery fails, you can enter the container and execute",
"常规定时": "Normal Timing",
"手动运行": "Manual Run",
"开机运行": "Boot Run",
"时区": "Timezone",
"强制打开": "Force Open",
"强制打开可能会导致编辑器显示异常": "Force opening may cause display issues in the editor",
"确认离开": "Confirm Leave",
"当前文件未保存,确认离开吗": "Current file is not saved, are you sure to leave?",
"收件邮箱地址,多个分号分隔,默认发送给发件邮箱地址": "Receiving email address, multiple semicolon separated, sent to the sending email address by default",
"选择备份模块": "Select backup module",
"开始备份": "Start backup",
"基础数据": "Basic data",
"脚本文件": "Script files",
"日志文件": "Log files",
"依赖缓存": "Dependency cache",
"远程脚本缓存": "Remote script cache",
"远程仓库缓存": "Remote repository cache",
"SSH 文件缓存": "SSH file cache",
"清除依赖缓存": "Clean dependency cache",
"清除成功": "Clean successful"
} }

Some files were not shown because too many files have changed in this diff Show More