Compare commits

...
81 Commits
Author SHA1 Message Date
whyour 55945e4cd1 更新版本 v2.17.2 2024-03-02 16:24:30 +08:00
whyour b39036f8f8 取消生成 core 文件 2024-03-02 16:22:18 +08:00
pharaoh2012andGitHub f07093d29f 企业微信有长度限制,超长的进行分段提交 (#2255) 2024-03-02 16:15:58 +08:00
whyour 11c789c71c 修复 webhook 通知 body 拆分逻辑 2024-02-25 15:27:48 +08:00
whyour 81898f9dd7 修复依赖安装失败状态变更 2024-02-14 22:28:32 +08:00
whyour 6dba8ae72d 依赖增加已取消状态 2024-02-14 21:54:53 +08:00
whyour c47896e787 修改依赖操作状态判断 2024-02-14 16:41:14 +08:00
whyour 14cb1f7788 依赖管理支持取消安装和状态筛选 2024-02-13 22:42:22 +08:00
whyour 892d91edc4 更新版本 v2.17.1 2024-02-13 15:19:52 +08:00
whyour 74916670eb 修复设置 nodejs 镜像源 2024-02-13 15:19:44 +08:00
whyour b31eab02d7 修复重启系统 2024-02-11 22:04:41 +08:00
whyour 9c258ab113 修复初始化更新镜像源配置 2024-02-11 21:46:27 +08:00
whyour 3bcba11b33 修改 workflow 编译 2024-02-08 22:10:11 +08:00
whyour 15140fd87d 静态资源编译增加 git gc 2024-02-08 21:28:46 +08:00
whyour e83058c3bc 兼容脚本、日志、配置文件详情接口 2024-02-08 20:47:03 +08:00
whyour 29f0c2c9ac 修复对比工具保存文件 2024-02-07 20:08:10 +08:00
whyour f25fe27473 脚本管理重命名增加默认值,增加新建、删除快捷键 2024-02-06 22:17:07 +08:00
whyour 0ae1f284ec 修改脚本详情和日志详情接口 2024-01-29 21:47:03 +08:00
whyour 49c441117a 修改 sqlite 版本 2024-01-28 20:22:55 +08:00
whyour 7bc5ff01a6 更新 readme 2024-01-25 23:54:03 +08:00
whyour 8128fb8a9f 修改依赖设置提示 2024-01-25 22:42:33 +08:00
秋澪andGitHub 315ae49360 添加发布端口和数据卷 (#2231) 2024-01-25 20:48:03 +08:00
Cp0204andGitHub 35c6475534 修复未开启一言时多余空行, 通知渠道改发送前检查 (#2222) 2024-01-25 13:14:26 +08:00
whyour 7f3e60423b 修复启动时依赖配置未初始化 2024-01-24 22:55:21 +08:00
whyour 0a723318a1 修复 workflow 缓存 2024-01-22 23:50:47 +08:00
whyour 2c1e7535b8 修复 debian 系统内更新源代码分支错误 2024-01-22 23:38:58 +08:00
whyour 48d1fba699 系统设置增加重启 2024-01-22 23:36:11 +08:00
whyour 06f861baaf 更新版本 v2.17.0 2024-01-22 22:51:33 +08:00
whyour bbbd353567 增加 python3.10 镜像 2024-01-22 22:51:22 +08:00
whyour 8283f62db2 修改获取配置文件接口,初始化系统配置 2024-01-18 13:51:06 +08:00
whyour 1a94a660a5 修改对比工具数据源,修复编辑器语言 2024-01-14 16:28:11 +08:00
whyour 5c393fef36 更新 readme 2024-01-13 21:34:13 +08:00
whyour 53346a3962 修复 notify.js smtp 通知 2024-01-13 20:52:04 +08:00
whyour 4e66a3aff5 修改 monaco-editor 静态资源地址 2024-01-10 22:12:41 +08:00
whyour b79d10b452 修改获取登录 ip 和 ip 地址方式 2024-01-08 19:05:12 +08:00
whyour 3dc3ece993 修改日志脚本文件大小展示 2024-01-07 16:54:05 +08:00
whyour 792cf9ad20 修复依赖设置样式 2024-01-02 22:07:12 +08:00
whyour 201f41e15b 修改镜像源设置样式 2024-01-01 22:33:20 +08:00
whyour 71e7ba9cb6 修改 nginx api 代理超时时间 2024-01-01 17:14:57 +08:00
whyour a7562259d1 更新 sqlite3 版本 2024-01-01 15:07:56 +08:00
whyour 18671264bf 移除 config.sh 中 PipMirror 和 NpmMirror配置 2023-12-25 22:05:33 +08:00
whyour 37bcb77be0 修复健康检查命令 2023-12-24 15:48:34 +08:00
whyour 563457cf1b JavaScript 通知文件 tunnel 替换为 hpagent,移除 scripts 目录下 package.json 2023-12-24 14:30:56 +08:00
whyour d2cf482d11 修复保存文件 2023-12-23 23:56:11 +08:00
whyour 2713942a68 脚本管理忽略符号文件 2023-12-23 00:28:31 +08:00
whyour 62168979e3 修复更新用户信息 2023-12-22 22:13:09 +08:00
whyour f8aba4b1fb 日志和脚本增加文件大小展示,修改脚本管理列表排序 2023-12-21 09:57:06 +08:00
whyour 52a89a21f6 降级 Alpine 为 3.18 2023-12-18 23:39:16 +08:00
whyour ca8790bbec 修复任务详情脚本行数展示 2023-12-17 17:53:15 +08:00
whyour be1d68c0d1 修改 schedule 服务代理环境变量 2023-12-10 21:53:23 +08:00
whyour 95afcbc9dd 更新 workflow 2023-12-09 21:29:04 +08:00
whyour 20249119a0 修复 sentry 配置 2023-12-09 12:02:58 +08:00
whyour 6e0ee6f627 移除 console 2023-12-05 23:23:13 +08:00
whyour 13e7dac333 修复自定义通知 body 中 key 被自动转为小写 2023-12-04 17:44:13 +08:00
whyour b89dc6dfc6 修改镜像源按钮增加 loading 2023-12-01 09:28:11 +08:00
whyour 897b710527 通知 TG_API_HOST 参数移除默认 https 协议,参数包含协议 2023-12-01 09:21:27 +08:00
whyour 5afec03548 修改获取切换 node 和 linux 源日志 2023-11-30 23:39:14 +08:00
whyour c6a25e5359 修改代理地址提示 2023-11-25 23:03:16 +08:00
whyour 3777a4e7b4 系统设置增加依赖代理和镜像设置 2023-11-25 21:49:33 +08:00
luckrnx09andGitHub 9b7d4cb8fe Enhance the script editor experience (#2186) 2023-11-25 14:24:08 +08:00
whyour 59f7b72bb1 移除 ql -l 参数 2023-11-23 19:40:52 +08:00
whyour 4d97f0f44d 重新安装依赖时不检查是否已安装 2023-11-22 22:54:36 +08:00
whyour de9e9ce627 webhook 通知 body 增加 text/plain 类型,修复多定时任务服务重启未初始化 2023-11-22 22:43:48 +08:00
whyour f056b611a4 修复删除依赖逻辑 2023-11-11 11:23:04 +08:00
whyour 659484511f 修复 js 通知文件 2023-11-10 22:57:17 +08:00
whyour 9ed107980c 系统设置增加依赖代理和镜像源设置 2023-11-06 23:55:16 +08:00
whyour 4a3ac7dc4b 移除随机延迟相关配置及 ProxyUrl 默认值,修复拉库通知可能失败 2023-11-05 01:02:31 +08:00
whyour c3a0f58003 更新 python 版本为 3.11 2023-11-04 21:21:21 +08:00
whyour 23781d8b34 修复获取日志列表 2023-11-03 21:31:48 +08:00
6dylan6andwhyour c1d6a0a40b 修复调用就结束执行的问题 2023-11-03 18:07:07 +08:00
Bad ChildandGitHub 82c9978514 修复 chronocat 通知成功判断 (#2166) 2023-11-03 17:57:10 +08:00
TungliesandGitHub 9f6d4943ea 修复: 邮件服务支持列表链接失效(#2167) 2023-11-03 17:55:59 +08:00
whyour c4a44a48c5 使用 npm 安装 pm2 tsx 2023-11-02 19:37:24 +08:00
whyour d2fef75e01 修改 trust proxy 2023-11-02 00:47:33 +08:00
whyour 085cb789b5 修复初始化设置通知,shell 映射,邮箱通知错误提示 2023-11-02 00:41:04 +08:00
whyour 20f615eadf fs 文件操作替换为 fs.promise 2023-11-01 16:44:34 +08:00
whyour 66a2769e7c 更新版本 v2.16.5 2023-10-30 15:04:16 +08:00
whyour 899a30eacd 修改重置环境变量逻辑 2023-10-30 15:04:14 +08:00
whyour 8760ac2964 修复定时任务详情日志列表样式 2023-10-26 23:03:58 +08:00
whyour 62a29867a3 更新 readme 2023-10-21 16:57:11 +08:00
whyour 4938635ef4 修复服务启动时环境变量污染 2023-10-20 17:54:32 +08:00
85 changed files with 2567 additions and 1250 deletions
+95 -29
View File
@@ -2,53 +2,51 @@ name: Build And Push Docker Image
on:
push:
paths-ignore:
- "*.md"
branches:
- 'master'
- 'develop'
- "master"
- "develop"
tags:
- 'v*'
- "v*"
schedule:
- cron: '00 20 * * *'
- cron: "00 20 * * *"
workflow_dispatch:
jobs:
to_gitlab:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url:
git@gitlab.com:whyour/qinglong.git
ssh_private_key:
${{ secrets.GITLAB_SSH_PK }}
target_repo_url: git@gitlab.com:whyour/qinglong.git
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
to_gitee:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url:
git@gitee.com:whyour/qinglong.git
ssh_private_key:
${{ secrets.GITLAB_SSH_PK }}
target_repo_url: git@gitee.com:whyour/qinglong.git
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
build-static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: '8.3.1'
version: "8.3.1"
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
cache: "pnpm"
- name: build front and back
run: |
@@ -80,6 +78,7 @@ jobs:
export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l git"
git remote add gitee "${REPO_GITEE}"
git remote add gitlab "${REPO_GITLAB}"
git gc
git push --force --quiet gitee ${GITHUB_BRANCH}:${GITHUB_BRANCH}
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH}
@@ -88,6 +87,7 @@ jobs:
# 由于 ubuntu-latest 中使用 linux6.x 内核 linux/s390x npm 无法使用
runs-on: ubuntu-20.04
# runs-on: self-hosted
permissions:
packages: write
@@ -95,17 +95,17 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: pnpm/action-setup@v3
with:
version: '8.3.1'
- uses: actions/setup-node@v3
version: "8.3.1"
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
cache: "pnpm"
- name: Setup timezone
uses: zcong1993/setup-timezone@master
uses: szenius/set-timezone@v1.2
with:
timezone: Asia/Shanghai
timezoneLinux: Asia/Shanghai
- name: Login to DockerHub
uses: docker/login-action@v3
@@ -146,7 +146,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
id: docker_build
uses: docker/build-push-action@v5
@@ -157,6 +157,7 @@ jobs:
SOURCE_COMMIT=${{ github.sha }}
network: host
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
# platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/386
context: .
file: ./docker/Dockerfile
push: true
@@ -164,6 +165,71 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=whyour/qinglong:cache
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
run: |
echo ${{ steps.docker_build.outputs.digest }}
build310:
if: ${{ github.ref_name == 'master' }}
needs: build-static
runs-on: ubuntu-20.04
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: "8.3.1"
- uses: actions/setup-node@v4
with:
cache: "pnpm"
- name: Setup timezone
uses: szenius/set-timezone@v1.2
with:
timezoneLinux: Asia/Shanghai
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push python3.10
id: docker_build_310
uses: docker/build-push-action@v5
with:
build-args: |
MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }}
SOURCE_COMMIT=${{ github.sha }}
network: host
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
context: .
file: ./docker/310.Dockerfile
push: true
tags: whyour/qinglong:python3.10
cache-from: type=registry,ref=whyour/qinglong:cache-python3.10
cache-to: type=registry,ref=whyour/qinglong:cache-python3.10,mode=max
- name: Image digest
run: |
echo ${{ steps.docker_build_310.outputs.digest }}
-7
View File
@@ -22,11 +22,4 @@
.env
.history
.version.ts
/config
/log
/db
/manual_log
/scripts
/bak
/.tmp
-1
View File
@@ -1,2 +1 @@
sentrycli_cdnurl=https://npmmirror.com/mirrors/sentry-cli/
strict-peer-dependencies=false
+1 -8
View File
@@ -21,12 +21,5 @@ src/.umi-production
src/.umi-test
.env.local
.env
history
version.ts
config
log
db
manual_log
scripts
bak
.tmp
/.tmp
+6
View File
@@ -47,4 +47,10 @@ export default defineConfig({
'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
],
copy: [
{
from: 'node_modules/monaco-editor/min/vs',
to: 'static/dist/monaco-editor/min/vs',
},
],
});
+22 -18
View File
@@ -56,7 +56,7 @@ npm i @whyour/qinglong
## Built-in commands
1. task
- task
```bash
# Execute in sequence, if a random delay is set, it will be randomly delayed by a certain number of seconds
@@ -71,13 +71,11 @@ task <file_path> conc <env_name> <account_number>(Optional)
task <file_path> desi <env_name> <account_number>
# Set task timeout
task -m <max_time> <file_path>
# Print task log in real time, no need to carry this parameter when creating timed tasks
task -l <file_path>
# 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
```
1. ql
- ql
```bash
# Update and restart Green Dragon
@@ -100,19 +98,17 @@ ql resetlet
ql resettfa
```
1. parameter description
- file_url: Script address
- repo_url: Repository address
- whitelist: The whitelist when pulling the repository, i.e., the string contained in the path of the script to be pulled
- blacklist: Blacklisting when pulling repositories, i.e. strings that are not included in the path of the script to be pulled
- dependence: Pulling the dependencies needed for the repository will be copied directly from the repository to the repository directory under scripts, regardless of the blacklist
- branch: Pull the branch of the repository
- days: Number of days of logs to be kept
- file_path: File path for task execution
- env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution
- account_number: Specify the account number of an environment variable to be executed when the task is executed
- max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
| **Parameter** | **Description** |
|---|---|
| file_url | Script address |
| repo_url | Repository address |
| whitelist | The whitelist when pulling the repository, i.e., the string contained in the path of the script to be pulled |
| blacklist | Blacklisting when pulling repositories, i.e. strings that are not included in the path of the script to be pulled |
| dependence | Pulling the dependencies needed for the repository will be copied directly from the repository to the repository directory under scripts, regardless of the blacklist |
| extensions | Pull the branch of the repository |
| branch | Number of days of logs to be kept |
| days | File path for task execution |
| file_path | The name of the environment variable that needs to be concurrent or specified at the time of task execution |
## Deployment
@@ -170,6 +166,14 @@ podman run -dit \
It is recommended to use a pure system installation to avoid losing the original system data, you need to install node/npm/python3/pip3 yourself
```bash
# Debian/Ubuntu
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
# Centos
curl --silent --location https://rpm.nodesource.com/setup_20.x | sudo bash
```
```bash
npm install -g node-pre-gyp pnpm@8.3.1
npm install -g @whyour/qinglong
qinglong
# Add the environment variables QL_DIR and QL_DATA_DIR when prompted
@@ -182,7 +186,7 @@ qinglong
## Development
```bash
$ git clone git@github.com:whyour/qinglong.git
$ git clone https://github.com/whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
# Recommended use pnpm https://pnpm.io/zh/installation
+23 -20
View File
@@ -58,7 +58,7 @@ npm i @whyour/qinglong
## 内置命令
1. task
- task
```bash
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
@@ -71,13 +71,11 @@ task <file_path> conc <env_name> <account_number>(可选的)
task <file_path> desi <env_name> <account_number>
# 设置任务超时时间
task -m <max_time> <file_path>
# 实时打印任务日志,创建定时任务时,不用携带此参数
task -l <file_path>
# 使用 -- 分割,-- 后面的参数会传给脚本,下面的例子,脚本就可接收到参数 -u whyour -p password
task <file_path> -- -u whyour -p password
```
1. ql
- ql
```bash
# 更新并重启青龙
@@ -100,20 +98,17 @@ ql resetlet
ql resettfa
```
1. 参数说明
- file_url: 脚本地址
- repo_url: 仓库地址
- whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割
- blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割
- dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割
- extensions: 拉取仓库的文件后缀,多个竖线分割
- branch: 拉取仓库的分支
- days: 需要保留的日志的天数
- file_path: 任务执行时的文件路径
- env_name: 任务执行时需要并发或者指定时的环境变量名称
- account_number: 任务执行时指定某个环境变量需要执行的账号序号
- max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
| **参数** | **说明** |
|------------|---------------------------------------------------------------------------------------------|
| file_url | 脚本地址 |
| repo_url | 仓库地址 |
| whitelist | 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割 |
| blacklist | 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割 |
| dependence | 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割 |
| extensions | 拉取仓库的文件后缀,多个竖线分割 |
| branch | 拉取仓库的分支 |
| days | 需要保留的日志的天数 |
| file_path | 任务执行时的文件路径 |
## 部署
@@ -171,6 +166,14 @@ podman run -dit \
建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3
```bash
# Debian/Ubuntu
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
# Centos
curl --silent --location https://rpm.nodesource.com/setup_20.x | sudo bash
```
```bash
npm install -g node-pre-gyp pnpm@8.3.1
npm install -g @whyour/qinglong
qinglong
# 根据提示增加环境变量 QL_DIR 和 QL_DATA_DIR
@@ -183,7 +186,7 @@ qinglong
## 开发
```bash
$ git clone git@github.com:whyour/qinglong.git
$ git clone https://github.com/whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
# 推荐使用 pnpm https://pnpm.io/zh/installation
@@ -192,7 +195,7 @@ $ pnpm install
$ pnpm start
```
打开你的浏览器,访问 http://127.0.0.1:5700
打开你的浏览器,访问 <http://127.0.0.1:5700>
## 链接
+38 -20
View File
@@ -3,20 +3,36 @@ import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import config from '../config';
import * as fs from 'fs';
import * as fs from 'fs/promises';
import { celebrate, Joi } from 'celebrate';
import { join } from 'path';
import { SAMPLE_FILES } from '../config/const';
import ConfigService from '../services/config';
const route = Router();
export default (app: Router) => {
app.use('/configs', route);
route.get(
'/sample',
async (req: Request, res: Response, next: NextFunction) => {
try {
res.send({
code: 200,
data: SAMPLE_FILES,
});
} catch (e) {
return next(e);
}
},
);
route.get(
'/files',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const fileList = fs.readdirSync(config.configPath, 'utf-8');
const fileList = await fs.readdir(config.configPath, 'utf-8');
res.send({
code: 200,
data: fileList
@@ -32,24 +48,11 @@ export default (app: Router) => {
);
route.get(
'/:file',
'/detail',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let content = '';
if (config.blackFileList.includes(req.params.file)) {
res.send({ code: 403, message: '文件无法访问' });
}
if (req.params.file.includes('sample')) {
content = getFileContentByName(
join(config.samplePath, req.params.file),
);
} else {
content = getFileContentByName(
join(config.configPath, req.params.file),
);
}
res.send({ code: 200, data: content });
const configService = Container.get(ConfigService);
await configService.getFile(req.query.path as string, res);
} catch (e) {
return next(e);
}
@@ -71,12 +74,27 @@ export default (app: Router) => {
if (config.blackFileList.includes(name)) {
res.send({ code: 403, message: '文件无法访问' });
}
const path = join(config.configPath, name);
fs.writeFileSync(path, content);
let path = join(config.configPath, name);
if (name.startsWith('data/scripts/')) {
path = join(config.rootPath, name);
}
await fs.writeFile(path, content);
res.send({ code: 200, message: '保存成功' });
} catch (e) {
return next(e);
}
},
);
route.get(
'/:file',
async (req: Request, res: Response, next: NextFunction) => {
try {
const configService = Container.get(ConfigService);
await configService.getFile(req.params.file, res);
} catch (e) {
return next(e);
}
},
);
};
+16
View File
@@ -134,4 +134,20 @@ export default (app: Router) => {
}
},
);
route.put(
'/cancel',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const dependenceService = Container.get(DependenceService);
await dependenceService.cancel(req.body);
return res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
};
+24 -9
View File
@@ -3,7 +3,7 @@ import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import { emptyDir, getFileContentByName, readDirs } from '../config/util';
import { getFileContentByName, readDirs, rmPath } from '../config/util';
import { join } from 'path';
import { celebrate, Joi } from 'celebrate';
const route = Router();
@@ -15,7 +15,7 @@ export default (app: Router) => {
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const result = readDirs(config.logPath, config.logPath, blacklist);
const result = await readDirs(config.logPath, config.logPath, blacklist);
res.send({
code: 200,
data: result,
@@ -26,10 +26,29 @@ export default (app: Router) => {
}
});
route.get(
'/detail',
async (req: Request, res: Response, next: NextFunction) => {
try {
if (blacklist.includes(req.path)) {
return res.send({ code: 403, message: '暂无权限' });
}
const filePath = join(
config.logPath,
(req.query.path || '') as string,
req.query.file as string,
);
const content = await getFileContentByName(filePath);
res.send({ code: 200, data: content });
} catch (e) {
return next(e);
}
},
);
route.get(
'/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
if (blacklist.includes(req.path)) {
return res.send({ code: 403, message: '暂无权限' });
@@ -39,7 +58,7 @@ export default (app: Router) => {
(req.query.path || '') as string,
req.params.file,
);
const content = getFileContentByName(filePath);
const content = await getFileContentByName(filePath);
res.send({ code: 200, data: content });
} catch (e) {
return next(e);
@@ -64,11 +83,7 @@ export default (app: Router) => {
type: string;
};
const filePath = join(config.logPath, path, filename);
if (type === 'directory') {
await emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
await rmPath(filePath);
res.send({ code: 200 });
} catch (e) {
return next(e);
+53 -24
View File
@@ -4,13 +4,13 @@ import {
readDirs,
getLastModifyFilePath,
readDir,
emptyDir,
rmPath,
} from '../config/util';
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import config from '../config';
import * as fs from 'fs';
import * as fs from 'fs/promises';
import { celebrate, Joi } from 'celebrate';
import path, { join, parse } from 'path';
import ScriptService from '../services/script';
@@ -34,15 +34,33 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger');
try {
let result = [];
const blacklist = ['node_modules', '.git', '.pnpm'];
const blacklist = [
'node_modules',
'.git',
'.pnpm',
'pnpm-lock.yaml',
'yarn.lock',
'package-lock.json',
];
if (req.query.path) {
const targetPath = path.join(
config.scriptPath,
req.query.path as string,
);
result = readDir(targetPath, config.scriptPath, blacklist);
result = await readDir(targetPath, config.scriptPath, blacklist);
} else {
result = readDirs(config.scriptPath, config.scriptPath, blacklist);
result = await readDirs(
config.scriptPath,
config.scriptPath,
blacklist,
(a, b) => {
if (a.type === b.type) {
return a.title.localeCompare(b.title);
} else {
return a.type === 'directory' ? -1 : 1;
}
},
);
}
res.send({
code: 200,
@@ -54,17 +72,31 @@ export default (app: Router) => {
}
});
route.get(
'/detail',
async (req: Request, res: Response, next: NextFunction) => {
try {
const scriptService = Container.get(ScriptService);
const content = await scriptService.getFile(
req.query.path as string,
req.query.file as string,
);
res.send({ code: 200, data: content });
} catch (e) {
return next(e);
}
},
);
route.get(
'/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const filePath = join(
config.scriptPath,
const scriptService = Container.get(ScriptService);
const content = await scriptService.getFile(
req.query.path as string,
req.params.file,
);
const content = getFileContentByName(filePath);
res.send({ code: 200, data: content });
} catch (e) {
return next(e);
@@ -104,12 +136,12 @@ export default (app: Router) => {
}
if (req.file) {
fs.renameSync(req.file.path, join(path, req.file.filename));
await fs.rename(req.file.path, join(path, req.file.filename));
return res.send({ code: 200 });
}
if (directory) {
fs.mkdirSync(join(path, directory), { recursive: true });
await fs.mkdir(join(path, directory), { recursive: true });
return res.send({ code: 200 });
}
@@ -121,16 +153,17 @@ export default (app: Router) => {
`${originFilename.replace(/\//g, '')}`,
);
const filePath = join(path, `${filename.replace(/\//g, '')}`);
if (fs.existsSync(originFilePath)) {
fs.copyFileSync(
const fileExists = await fileExist(filePath);
if (fileExists) {
await fs.copyFile(
originFilePath,
join(config.bakPath, originFilename.replace(/\//g, '')),
);
if (filename !== originFilename) {
fs.unlinkSync(originFilePath);
await rmPath(originFilePath);
}
}
fs.writeFileSync(filePath, content);
await fs.writeFile(filePath, content);
return res.send({ code: 200 });
} catch (e) {
return next(e);
@@ -156,7 +189,7 @@ export default (app: Router) => {
path: string;
};
const filePath = join(config.scriptPath, path, filename);
fs.writeFileSync(filePath, content);
await fs.writeFile(filePath, content);
return res.send({ code: 200 });
} catch (e) {
return next(e);
@@ -182,11 +215,7 @@ export default (app: Router) => {
type: string;
};
const filePath = join(config.scriptPath, path, filename);
if (type === 'directory') {
await emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
await rmPath(filePath);
res.send({ code: 200 });
} catch (e) {
return next(e);
@@ -239,7 +268,7 @@ export default (app: Router) => {
let { filename, content, path } = req.body;
const { name, ext } = parse(filename);
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
fs.writeFileSync(filePath, content || '', { encoding: 'utf8' });
await fs.writeFile(filePath, content || '', { encoding: 'utf8' });
const scriptService = Container.get(ScriptService);
const result = await scriptService.runScript(filePath);
@@ -269,7 +298,7 @@ export default (app: Router) => {
const scriptService = Container.get(ScriptService);
const result = await scriptService.stopScript(filePath, pid);
setTimeout(() => {
emptyDir(logPath);
rmPath(logPath);
}, 3000);
res.send(result);
} catch (e) {
@@ -297,7 +326,7 @@ export default (app: Router) => {
};
const filePath = join(config.scriptPath, path, filename);
const newPath = join(config.scriptPath, path, newFilename);
fs.renameSync(filePath, newPath);
await fs.rename(filePath, newPath);
res.send({ code: 200 });
} catch (e) {
return next(e);
+106 -23
View File
@@ -1,12 +1,11 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import * as fs from 'fs/promises';
import config from '../config';
import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate';
import UserService from '../services/user';
import { EnvModel } from '../data/env';
import {
getUniqPath,
handleLogPath,
@@ -78,18 +77,16 @@ export default (app: Router) => {
);
route.put(
'/config',
'/config/log-remove-frequency',
celebrate({
body: Joi.object({
logRemoveFrequency: Joi.number().optional().allow(null),
cronConcurrency: Joi.number().optional().allow(null),
logRemoveFrequency: Joi.number().allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.updateSystemConfig(req.body);
const result = await systemService.updateLogRemoveFrequency(req.body);
res.send(result);
} catch (e) {
return next(e);
@@ -97,6 +94,96 @@ export default (app: Router) => {
},
);
route.put(
'/config/cron-concurrency',
celebrate({
body: Joi.object({
cronConcurrency: Joi.number().allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.updateCronConcurrency(req.body);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/config/dependence-proxy',
celebrate({
body: Joi.object({
dependenceProxy: Joi.string().allow('').allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.updateDependenceProxy(req.body);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/config/node-mirror',
celebrate({
body: Joi.object({
nodeMirror: Joi.string().allow('').allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
res.setHeader('Content-type', 'application/octet-stream');
await systemService.updateNodeMirror(req.body, res);
} catch (e) {
return next(e);
}
},
);
route.put(
'/config/python-mirror',
celebrate({
body: Joi.object({
pythonMirror: Joi.string().allow('').allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.updatePythonMirror(req.body);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/config/linux-mirror',
celebrate({
body: Joi.object({
linuxMirror: Joi.string().allow('').allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
res.setHeader('Content-type', 'application/octet-stream');
await systemService.updateLinuxMirror(req.body, res);
} catch (e) {
return next(e);
}
},
);
route.put(
'/update-check',
async (req: Request, res: Response, next: NextFunction) => {
@@ -129,7 +216,7 @@ export default (app: Router) => {
'/reload',
celebrate({
body: Joi.object({
type: Joi.string().required(),
type: Joi.string().optional().allow('').allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@@ -174,7 +261,7 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const command = req.body.command
const command = req.body.command;
const idStr = `cat ${config.crontabFile} | grep -E "${command}" | perl -pe "s|.*ID=(.*) ${command}.*|\\1|" | head -1 | awk -F " " '{print $1}' | xargs echo -n`;
let id = await promiseExec(idStr);
const uniqPath = await getUniqPath(command, id);
@@ -193,12 +280,12 @@ export default (app: Router) => {
onError: async (message: string) => {
res.write(`\n${message}`);
const absolutePath = await handleLogPath(logPath);
fs.appendFileSync(absolutePath, `\n${message}`);
await fs.appendFile(absolutePath, `\n${message}`);
},
onLog: async (message: string) => {
res.write(`\n${message}`);
const absolutePath = await handleLogPath(logPath);
fs.appendFileSync(absolutePath, `\n${message}`);
await fs.appendFile(absolutePath, `\n${message}`);
},
},
);
@@ -253,16 +340,12 @@ export default (app: Router) => {
},
);
route.get(
'/log',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
await systemService.getSystemLog(res);
} catch (e) {
return next(e);
}
},
);
route.get('/log', async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
await systemService.getSystemLog(res);
} catch (e) {
return next(e);
}
});
};
+3
View File
@@ -74,6 +74,9 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
if (process.env.DeployEnv === 'demo') {
return res.send({ code: 450, message: '未知错误' });
}
const userService = Container.get(UserService);
await userService.updateUsernameAndPassword(req.body);
res.send({ code: 200, message: '更新成功' });
+18
View File
@@ -5,3 +5,21 @@ export const QL_COMMAND = 'ql';
export const TASK_PREFIX = `${TASK_COMMAND} `;
export const QL_PREFIX = `${QL_COMMAND} `;
export const SAMPLE_FILES = [
{
title: 'config.sample.sh',
value: 'sample/config.sample.sh',
target: 'config.sh',
},
{
title: 'notify.js',
value: 'sample/notify.js',
target: 'data/scripts/sendNotify.js',
},
{
title: 'notify.py',
value: 'sample/notify.py',
target: 'data/scripts/notify.py',
},
];
+3
View File
@@ -47,6 +47,7 @@ const configString = 'config sample crontab shareCode diy';
const versionFile = path.join(rootPath, 'version.yaml');
const dataTgzFile = path.join(tmpPath, 'data.tgz');
const shareShellFile = path.join(shellPath, 'share.sh');
const dependenceProxyFile = path.join(configPath, 'dependence-proxy.sh');
if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️");
@@ -68,6 +69,7 @@ export default {
dataPath,
dataTgzFile,
shareShellFile,
dependenceProxyFile,
configString,
loginFaild,
authError,
@@ -89,6 +91,7 @@ export default {
'config.sh.sample',
'cookie.sh',
'crontab.list',
'dependence-proxy.sh',
'env.sh',
'token.json',
],
+141 -123
View File
@@ -1,4 +1,4 @@
import * as fs from 'fs';
import * as fs from 'fs/promises';
import * as path from 'path';
import got from 'got';
import iconv from 'iconv-lite';
@@ -13,22 +13,24 @@ import Logger from '../loaders/logger';
export * from './share';
export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) {
return fs.readFileSync(fileName, 'utf8');
export async function getFileContentByName(fileName: string) {
const _exsit = await fileExist(fileName);
if (_exsit) {
return await fs.readFile(fileName, 'utf8');
}
return '';
}
export function getLastModifyFilePath(dir: string) {
export async function getLastModifyFilePath(dir: string) {
let filePath = '';
if (fs.existsSync(dir)) {
const arr = fs.readdirSync(dir);
const _exsit = await fileExist(dir);
if (_exsit) {
const arr = await fs.readdir(dir);
arr.forEach((item) => {
arr.forEach(async (item) => {
const fullpath = path.join(dir, item);
const stats = fs.statSync(fullpath);
const stats = await fs.stat(fullpath);
if (stats.isFile()) {
if (stats.mtimeMs >= 0) {
filePath = fullpath;
@@ -59,7 +61,8 @@ export async function getNetIp(req: any) {
...req.ips,
req.socket.remoteAddress,
]),
];
].filter(Boolean);
let ip = ipArray[0];
if (ipArray.length > 1) {
@@ -80,6 +83,7 @@ export async function getNetIp(req: any) {
break;
}
}
ip = ip.substr(ip.lastIndexOf(':') + 1, ip.length);
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
ip = '';
@@ -90,29 +94,31 @@ export async function getNetIp(req: any) {
}
try {
const baiduApi = got
.get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
const csdnApi = got
.get(`https://searchplugin.csdn.net/api/v1/ip/get?ip=${ip}`, {
timeout: 10000,
retry: 0,
})
.text();
const ipApi = got
const pconlineApi = got
.get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, {
timeout: 10000,
retry: 0,
})
.buffer();
const [data, ipApiBody] = await await Promise.all<any>([baiduApi, ipApi]);
const ipRegx = /.*IP :(.*)\n/;
const addrRegx = /.*数据二 :(.*)\n/;
if (data && ipRegx.test(data) && addrRegx.test(data)) {
const ip = data.match(ipRegx)[1];
const addr = data.match(addrRegx)[1];
return { address: addr, ip };
} else if (ipApiBody) {
const { addr, ip } = JSON.parse(iconv.decode(ipApiBody, 'GBK'));
return { address: `${addr}`, ip };
} else {
return { address: `获取失败`, ip };
const [csdnBody, pconlineBody] = await await Promise.all<any>([
csdnApi,
pconlineApi,
]);
const csdnRes = JSON.parse(csdnBody);
const pconlineRes = JSON.parse(iconv.decode(pconlineBody, 'GBK'));
let address = '';
if (csdnBody && csdnRes.code == 200) {
address = csdnRes.data.address;
} else if (pconlineRes && pconlineRes.addr) {
address = pconlineRes.addr;
}
return { address, ip };
} catch (error) {
return { address: `获取失败`, ip };
}
@@ -152,22 +158,17 @@ export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
}
export async function fileExist(file: any) {
return new Promise((resolve) => {
try {
fs.accessSync(file);
resolve(true);
} catch (error) {
resolve(false);
}
});
try {
await fs.access(file);
return true;
} catch (error) {
return false;
}
}
export async function createFile(file: string, data: string = '') {
return new Promise((resolve) => {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, data);
resolve(true);
});
await fs.mkdir(path.dirname(file), { recursive: true });
await fs.writeFile(file, data);
}
export async function handleLogPath(
@@ -235,63 +236,71 @@ interface IFile {
}
export function dirSort(a: IFile, b: IFile): number {
if (a.type !== b.type) {
return FileType[a.type] < FileType[b.type] ? -1 : 1
} else if (a.mtime !== b.mtime) {
return a.mtime > b.mtime ? -1 : 1
if (a.type === 'file' && b.type === 'file') {
return b.mtime - a.mtime;
} else if (a.type === 'directory' && b.type === 'directory') {
return a.title.localeCompare(b.title);
} else {
return 0;
return a.type === 'directory' ? -1 : 1;
}
}
export function readDirs(
export async function readDirs(
dir: string,
baseDir: string = '',
blacklist: string[] = [],
): IFile[] {
sort: (a: IFile, b: IFile) => number = dirSort,
): Promise<IFile[]> {
const relativePath = path.relative(baseDir, dir);
const files = fs.readdirSync(dir);
const result: IFile[] = files
.filter((x) => !blacklist.includes(x))
.map((file: string) => {
const subPath = path.join(dir, file);
const stats = fs.statSync(subPath);
const key = path.join(relativePath, file);
if (stats.isDirectory()) {
return {
title: file,
key,
type: 'directory',
parent: relativePath,
mtime: stats.mtime.getTime(),
children: readDirs(subPath, baseDir).sort(dirSort),
};
}
return {
const files = await fs.readdir(dir);
const result: IFile[] = [];
for (const file of files) {
const subPath = path.join(dir, file);
const stats = await fs.stat(subPath);
const key = path.join(relativePath, file);
if (blacklist.includes(file) || stats.isSymbolicLink()) {
continue;
}
if (stats.isDirectory()) {
const children = await readDirs(subPath, baseDir, blacklist, sort);
result.push({
title: file,
key,
type: 'directory',
parent: relativePath,
mtime: stats.mtime.getTime(),
children: children.sort(sort),
});
} else {
result.push({
title: file,
type: 'file',
isLeaf: true,
key,
parent: relativePath,
size: stats.size,
mtime: stats.mtime.getTime(),
};
});
return result.sort(dirSort);
});
}
}
return result.sort(sort);
}
export function readDir(
export async function readDir(
dir: string,
baseDir: string = '',
blacklist: string[] = [],
) {
const relativePath = path.relative(baseDir, dir);
const files = fs.readdirSync(dir);
const files = await fs.readdir(dir);
const result: any = files
.filter((x) => !blacklist.includes(x))
.map((file: string) => {
.map(async (file: string) => {
const subPath = path.join(dir, file);
const stats = fs.statSync(subPath);
const stats = await fs.stat(subPath);
const key = path.join(relativePath, file);
return {
title: file,
@@ -303,27 +312,12 @@ export function readDir(
return result;
}
export async function emptyDir(path: string) {
const pathExist = await fileExist(path);
if (!pathExist) {
return;
}
const files = fs.readdirSync(path);
for (const file of files) {
const filePath = `${path}/${file}`;
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
await emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
}
fs.rmdirSync(path);
}
export async function promiseExec(command: string): Promise<string> {
try {
const { stderr, stdout } = await promisify(exec)(command, { maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' });
const { stderr, stdout } = await promisify(exec)(command, {
maxBuffer: 200 * 1024 * 1024,
encoding: 'utf8',
});
return stdout || stderr;
} catch (error) {
return JSON.stringify(error);
@@ -332,7 +326,10 @@ export async function promiseExec(command: string): Promise<string> {
export async function promiseExecSuccess(command: string): Promise<string> {
try {
const { stdout } = await promisify(exec)(command, { maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' });
const { stdout } = await promisify(exec)(command, {
maxBuffer: 200 * 1024 * 1024,
encoding: 'utf8',
});
return stdout || '';
} catch (error) {
return '';
@@ -363,37 +360,44 @@ export function parseHeaders(headers: string) {
return parsed;
}
function parseString(input: string): Record<string, string> {
const regex = /(\w+):\s*((?:(?!\n\w+:).)*)/g;
const matches: Record<string, string> = {};
let match;
while ((match = regex.exec(input)) !== null) {
const [, key, value] = match;
const _key = key.trim();
if (!_key || matches[_key]) {
continue;
}
const _value = value.trim();
try {
const jsonValue = JSON.parse(_value);
matches[_key] = jsonValue;
} catch (error) {
matches[_key] = _value;
}
}
return matches;
}
export function parseBody(
body: string,
contentType:
| 'application/json'
| 'multipart/form-data'
| 'application/x-www-form-urlencoded',
| 'application/x-www-form-urlencoded'
| 'text/plain',
) {
if (!body) return '';
if (contentType === 'text/plain' || !body) {
return body;
}
const parsed: any = {};
let key;
let val;
let i;
body &&
body.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key || parsed[key]) {
return;
}
try {
const jsonValue = JSON.parse(val);
parsed[key] = jsonValue;
} catch (error) {
parsed[key] = val;
}
});
const parsed = parseString(body);
switch (contentType) {
case 'multipart/form-data':
@@ -429,14 +433,14 @@ export async function killTask(pid: number) {
[pid, ...pids].reverse().forEach((x) => {
process.kill(x, 15);
});
} catch (error) { }
} catch (error) {}
} else {
process.kill(pid, 2);
}
}
export async function getPid(name: string) {
const taskCommand = `ps -eo pid,command | grep "${name}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`;
export async function getPid(cmd: string) {
const taskCommand = `ps -eo pid,command | grep "${cmd}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`;
const pid = await promiseExec(taskCommand);
return pid ? Number(pid) : undefined;
}
@@ -449,14 +453,17 @@ interface IVersion {
}
export async function parseVersion(path: string): Promise<IVersion> {
return load(await promisify(fs.readFile)(path, 'utf8')) as IVersion;
return load(await fs.readFile(path, 'utf8')) as IVersion;
}
export async function parseContentVersion(content: string): Promise<IVersion> {
return load(content) as IVersion;
}
export async function getUniqPath(command: string, id: string): Promise<string> {
export async function getUniqPath(
command: string,
id: string,
): Promise<string> {
if (/^\d+$/.test(id)) {
id = `_${id}`;
} else {
@@ -498,7 +505,18 @@ export function safeJSONParse(value?: string) {
try {
return JSON.parse(value);
} catch (error) {
Logger.error('[JSON.parse失败]', error)
Logger.error('[JSON.parse失败]', error);
return {};
}
}
}
export async function rmPath(path: string) {
try {
const _exsit = await fileExist(path);
if (_exsit) {
await fs.rm(path, { force: true, recursive: true, maxRetries: 5 });
}
} catch (error) {
Logger.error('[rmPath失败]', error);
}
}
+1
View File
@@ -32,6 +32,7 @@ export enum DependenceStatus {
'removed',
'removeFailed',
'queued',
'cancelled',
}
export enum DependenceTypes {
+2 -2
View File
@@ -66,7 +66,7 @@ export class TelegramBotNotification extends NotificationBaseInfo {
public telegramBotProxyHost = '';
public telegramBotProxyPort = '';
public telegramBotProxyAuth = '';
public telegramBotApiHost = 'api.telegram.org';
public telegramBotApiHost = 'https://api.telegram.org';
}
export class DingtalkBotNotification extends NotificationBaseInfo {
@@ -112,7 +112,7 @@ export class PushMeNotification extends NotificationBaseInfo {
export class ChronocatNotification extends NotificationBaseInfo {
public chronocatURL: string = '';
public chronocatQQ: string = '';
public chronocatToekn: string = '';
public chronocatToken: string = '';
}
export class WebhookNotification extends NotificationBaseInfo {
+3 -1
View File
@@ -17,4 +17,6 @@ export type SockMessageType =
| 'updateSystemVersion'
| 'manuallyRunScript'
| 'runSubscriptionEnd'
| 'reloadSystem';
| 'reloadSystem'
| 'updateNodeMirror'
| 'updateLinuxMirror';
+8 -4
View File
@@ -5,7 +5,7 @@ import { NotificationInfo } from './notify';
export class AuthInfo {
ip?: string;
type: AuthDataType;
info?: AuthModelInfo;
info?: SystemModelInfo;
id?: number;
constructor(options: AuthInfo) {
@@ -32,6 +32,10 @@ export enum AuthDataType {
export interface SystemConfigInfo {
logRemoveFrequency?: number;
cronConcurrency?: number;
dependenceProxy?: string;
nodeMirror?: string;
pythonMirror?: string;
linuxMirror?: string;
}
export interface LoginLogInfo {
@@ -42,12 +46,12 @@ export interface LoginLogInfo {
status?: LoginStatus;
}
export type AuthModelInfo = SystemConfigInfo &
export type SystemModelInfo = SystemConfigInfo &
Partial<NotificationInfo> &
LoginLogInfo;
export interface AuthInstance extends Model<AuthInfo, AuthInfo>, AuthInfo {}
export const AuthModel = sequelize.define<AuthInstance>('Auth', {
export interface SystemInstance extends Model<AuthInfo, AuthInfo>, AuthInfo { }
export const SystemModel = sequelize.define<SystemInstance>('Auth', {
ip: DataTypes.STRING,
type: DataTypes.STRING,
info: {
+4 -4
View File
@@ -5,7 +5,7 @@ import { EnvModel } from '../data/env';
import { CrontabModel } from '../data/cron';
import { DependenceModel } from '../data/dependence';
import { AppModel } from '../data/open';
import { AuthModel } from '../data/auth';
import { SystemModel } from '../data/system';
import { fileExist } from '../config/util';
import { SubscriptionModel } from '../data/subscription';
import { CrontabViewModel } from '../data/cronView';
@@ -17,7 +17,7 @@ export default async () => {
await CrontabModel.sync();
await DependenceModel.sync();
await AppModel.sync();
await AuthModel.sync();
await SystemModel.sync();
await EnvModel.sync();
await SubscriptionModel.sync();
await CrontabViewModel.sync();
@@ -75,7 +75,7 @@ export default async () => {
const dependenceCount = await DependenceModel.count();
const envCount = await EnvModel.count();
const appCount = await AppModel.count();
const authCount = await AuthModel.count();
const authCount = await SystemModel.count();
if (crondbExist && cronCount === 0) {
const cronDb = new DataStore({
filename: cronDbFile,
@@ -127,7 +127,7 @@ export default async () => {
});
authDb.persistence.compactDatafile();
authDb.find({}).exec(async (err, docs) => {
await AuthModel.bulkCreate(docs, { ignoreDuplicates: true });
await SystemModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
+11 -14
View File
@@ -1,20 +1,19 @@
import path from 'path';
import fs from 'fs';
import fs from 'fs/promises';
import chokidar from 'chokidar';
import config from '../config/index';
import { promiseExec } from '../config/util';
import { fileExist, promiseExec, rmPath } from '../config/util';
function linkToNodeModule(src: string, dst?: string) {
async function linkToNodeModule(src: string, dst?: string) {
const target = path.join(config.rootPath, 'node_modules', dst || src);
const source = path.join(config.rootPath, src);
fs.lstat(target, (err, stat) => {
if (!stat) {
fs.symlink(source, target, 'dir', (err) => {
if (err) throw err;
});
try {
const stats = await fs.lstat(target);
if (!stats) {
await fs.symlink(source, target, 'dir');
}
});
} catch (error) {}
}
async function linkCommand() {
@@ -34,16 +33,14 @@ async function linkCommand() {
for (const link of linkShell) {
const source = path.join(config.rootPath, 'shell', link.src);
const target = path.join(commandDir, link.dest);
if (fs.existsSync(target)) {
fs.unlinkSync(target);
}
fs.symlink(source, target, (err) => { });
await rmPath(target);
await fs.symlink(source, target);
}
}
export default async (src: string = 'deps') => {
await linkCommand();
linkToNodeModule(src);
await linkToNodeModule(src);
const source = path.join(config.rootPath, src);
const watcher = chokidar.watch(source, {
+4 -5
View File
@@ -4,7 +4,7 @@ import cors from 'cors';
import routes from '../api';
import config from '../config';
import jwt, { UnauthorizedError } from 'express-jwt';
import fs from 'fs';
import fs from 'fs/promises';
import { getPlatform, getToken, safeJSONParse } from '../config/util';
import Container from 'typedi';
import OpenService from '../services/open';
@@ -18,7 +18,7 @@ import { serveEnv } from '../config/serverEnv';
import Logger from './logger';
export default ({ app }: { app: Application }) => {
app.enable('trust proxy');
app.set('trust proxy', 'loopback');
app.use(cors());
app.get(`${config.api.prefix}/env.js`, serveEnv);
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
@@ -29,7 +29,7 @@ export default ({ app }: { app: Application }) => {
target: `http://0.0.0.0:${config.publicPort}/api`,
changeOrigin: true,
pathRewrite: { '/api/public': '' },
logProvider: () => Logger
logProvider: () => Logger,
}),
);
@@ -83,7 +83,7 @@ export default ({ app }: { app: Application }) => {
return next();
}
const data = fs.readFileSync(config.authConfigFile, 'utf8');
const data = await fs.readFile(config.authConfigFile, 'utf8');
if (data && headerToken) {
const { token = '', tokens = {} } = safeJSONParse(data);
if (headerToken === token || tokens[req.platform] === headerToken) {
@@ -105,7 +105,6 @@ export default ({ app }: { app: Application }) => {
}
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
const envCount = await EnvModel.count();
let isInitialized = true;
if (
+50 -16
View File
@@ -9,11 +9,58 @@ import { Op } from 'sequelize';
import config from '../config';
import { CrontabViewModel, CronViewType } from '../data/cronView';
import { initPosition } from '../data/env';
import { AuthDataType, SystemModel } from '../data/system';
import SystemService from '../services/system';
export default async () => {
const cronService = Container.get(CronService);
const envService = Container.get(EnvService);
const dependenceService = Container.get(DependenceService);
const systemService = Container.get(SystemService);
const installDependencies = () => {
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
DependenceModel.findAll({
where: {},
order: [
['type', 'DESC'],
['createdAt', 'DESC'],
],
raw: true,
}).then(async (docs) => {
await DependenceModel.update(
{ status: DependenceStatus.queued, log: [] },
{ where: { id: docs.map((x) => x.id!) } },
);
setTimeout(() => {
dependenceService.installDependenceOneByOne(docs);
}, 5000);
});
};
// 初始化更新 linux/python/nodejs 镜像源配置
const systemConfig = await systemService.getSystemConfig();
if (systemConfig.info?.pythonMirror) {
systemService.updatePythonMirror({
pythonMirror: systemConfig.info?.pythonMirror,
});
}
if (systemConfig.info?.linuxMirror) {
systemService.updateLinuxMirror(
{
linuxMirror: systemConfig.info?.linuxMirror,
},
undefined,
() => installDependencies(),
);
} else {
installDependencies();
}
if (systemConfig.info?.nodeMirror) {
systemService.updateNodeMirror({
nodeMirror: systemConfig.info?.nodeMirror,
});
}
// 初始化新增默认全部任务视图
CrontabViewModel.findAll({
@@ -32,22 +79,6 @@ export default async () => {
// 初始化更新所有任务状态为空闲
await CrontabModel.update({ status: CrontabStatus.idle }, { where: {} });
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
DependenceModel.findAll({
where: {},
order: [
['type', 'DESC'],
['createdAt', 'DESC'],
],
raw: true,
}).then(async (docs) => {
await DependenceModel.update(
{ status: DependenceStatus.queued, log: [] },
{ where: { id: docs.map((x) => x.id!) } },
);
dependenceService.installDependenceOneByOne(docs);
});
// 初始化时执行一次所有的 ql repo 任务
CrontabModel.findAll({
where: {
@@ -127,4 +158,7 @@ export default async () => {
// 初始化保存一次ck和定时任务数据
await cronService.autosave_crontab();
await envService.set_envs();
// 初始化增加系统配置
await SystemModel.upsert({ type: AuthDataType.systemConfig });
};
+22 -19
View File
@@ -1,7 +1,6 @@
import fs from 'fs';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import dotenv from 'dotenv';
import Logger from './logger';
import { fileExist } from '../config/util';
@@ -48,68 +47,72 @@ export default async () => {
const TaskAfterFileExist = await fileExist(TaskAfterFile);
if (!configDirExist) {
fs.mkdirSync(configPath);
await fs.mkdir(configPath);
}
if (!scriptDirExist) {
fs.mkdirSync(scriptPath);
await fs.mkdir(scriptPath);
}
if (!logDirExist) {
fs.mkdirSync(logPath);
await fs.mkdir(logPath);
}
if (!tmpDirExist) {
fs.mkdirSync(tmpPath);
await fs.mkdir(tmpPath);
}
if (!uploadDirExist) {
fs.mkdirSync(uploadPath);
await fs.mkdir(uploadPath);
}
if (!sshDirExist) {
fs.mkdirSync(sshPath);
await fs.mkdir(sshPath);
}
if (!bakDirExist) {
fs.mkdirSync(bakPath);
await fs.mkdir(bakPath);
}
if (!sshdDirExist) {
fs.mkdirSync(sshdPath);
await fs.mkdir(sshdPath);
}
if (!systemLogDirExist) {
fs.mkdirSync(systemLogPath);
await fs.mkdir(systemLogPath);
}
// 初始化文件
if (!authFileExist) {
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
await fs.writeFile(authConfigFile, await fs.readFile(sampleAuthFile));
}
if (!confFileExist) {
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
await fs.writeFile(confFile, await fs.readFile(sampleConfigFile));
}
if (!scriptNotifyJsFileExist) {
fs.writeFileSync(scriptNotifyJsFile, fs.readFileSync(sampleNotifyJsFile));
await fs.writeFile(
scriptNotifyJsFile,
await fs.readFile(sampleNotifyJsFile),
);
}
if (!scriptNotifyPyFileExist) {
fs.writeFileSync(scriptNotifyPyFile, fs.readFileSync(sampleNotifyPyFile));
await fs.writeFile(
scriptNotifyPyFile,
await fs.readFile(sampleNotifyPyFile),
);
}
if (!TaskBeforeFileExist) {
fs.writeFileSync(TaskBeforeFile, fs.readFileSync(sampleTaskShellFile));
await fs.writeFile(TaskBeforeFile, await fs.readFile(sampleTaskShellFile));
}
if (!TaskAfterFileExist) {
fs.writeFileSync(TaskAfterFile, fs.readFileSync(sampleTaskShellFile));
await fs.writeFile(TaskAfterFile, await fs.readFile(sampleTaskShellFile));
}
dotenv.config({ path: confFile });
Logger.info('✌️ Init file down');
console.log('✌️ Init file down');
};
+2 -3
View File
@@ -1,6 +1,5 @@
import { Application } from 'express';
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import Logger from './logger';
import config from '../config';
import fs from 'fs';
@@ -16,10 +15,10 @@ export default async ({ expressApp }: { expressApp: Application }) => {
/UnauthorizedError/i,
/celebrate request validation failed/i,
],
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
dsn: 'https://8b5c84cfef3e22541bc84de0ed00497b@o1098464.ingest.sentry.io/6122819',
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app: expressApp }),
new Sentry.Integrations.Express({ app: expressApp }),
],
tracesSampleRate: 0.8,
release: version,
+3 -4
View File
@@ -1,22 +1,21 @@
import sockJs from 'sockjs';
import { Server } from 'http';
import Logger from './logger';
import { Container } from 'typedi';
import SockService from '../services/sock';
import config from '../config/index';
import fs from 'fs';
import fs from 'fs/promises';
import { getPlatform, safeJSONParse } from '../config/util';
export default async ({ server }: { server: Server }) => {
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
const sockService = Container.get(SockService);
echo.on('connection', (conn) => {
echo.on('connection', async (conn) => {
if (!conn.headers || !conn.url || !conn.pathname) {
conn.close('404');
}
const data = fs.readFileSync(config.authConfigFile, 'utf8');
const data = await fs.readFile(config.authConfigFile, 'utf8');
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
if (data) {
+1
View File
@@ -8,6 +8,7 @@ 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) => {
+1
View File
@@ -12,6 +12,7 @@ class Client {
private client = new CronClient(
`0.0.0.0:${config.cronPort}`,
credentials.createInsecure(),
{ 'grpc.enable_http_proxy': 0 },
);
addCron(request: AddCronRequest['crons']): Promise<AddCronResponse> {
+1 -1
View File
@@ -10,7 +10,7 @@ const check = async (
switch (call.request.service) {
case 'cron':
const res = await promiseExec(
`curl -s http://0.0.0.0:${config.port}/api/system`,
`curl -s --noproxy '*' http://0.0.0.0:${config.port}/api/system`,
);
if (res.includes('200')) {
+1 -1
View File
@@ -7,7 +7,7 @@ import { check } from './health';
import config from '../config';
import Logger from '../loaders/logger';
const server = new Server();
const server = new Server({ 'grpc.enable_http_proxy': 0 });
server.addService(HealthService, { check });
server.addService(CronService, { addCron, delCron });
server.bindAsync(
+30
View File
@@ -0,0 +1,30 @@
import { Service, Inject } from 'typedi';
import path, { join } from 'path';
import config from '../config';
import { getFileContentByName } from '../config/util';
import { Response } from 'express';
import got from 'got';
@Service()
export default class ConfigService {
constructor() {}
public async getFile(filePath: string, res: Response) {
let content = '';
if (config.blackFileList.includes(filePath) || !filePath) {
res.send({ code: 403, message: '文件无法访问' });
}
if (filePath.startsWith('sample/')) {
const res = await got.get(
`https://gitlab.com/whyour/qinglong/-/raw/master/${filePath}`,
);
content = res.body;
} else if (filePath.startsWith('data/scripts/')) {
content = await getFileContentByName(join(config.rootPath, filePath));
} else {
content = await getFileContentByName(join(config.configPath, filePath));
}
res.send({ code: 200, data: content });
}
}
+74 -41
View File
@@ -3,10 +3,15 @@ import winston from 'winston';
import config from '../config';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import { exec, execSync } from 'child_process';
import fs from 'fs';
import fs from 'fs/promises';
import cron_parser from 'cron-parser';
import { getFileContentByName, fileExist, killTask, getUniqPath, safeJSONParse } from '../config/util';
import { promises, existsSync } from 'fs';
import {
getFileContentByName,
fileExist,
killTask,
getUniqPath,
safeJSONParse,
} from '../config/util';
import { Op, where, col as colFn, FindOptions, fn } from 'sequelize';
import path from 'path';
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
@@ -19,11 +24,11 @@ import omit from 'lodash/omit';
@Service()
export default class CronService {
constructor(@Inject('logger') private logger: winston.Logger) { }
constructor(@Inject('logger') private logger: winston.Logger) {}
private isSixCron(cron: Crontab) {
const { schedule } = cron;
if (Number(schedule?.split(/ +/).length) > 5) {
private isNodeCron(cron: Crontab) {
const { schedule, extra_schedules } = cron;
if (Number(schedule?.split(/ +/).length) > 5 || extra_schedules?.length) {
return true;
}
return false;
@@ -33,9 +38,15 @@ export default class CronService {
const tab = new Crontab(payload);
tab.saved = false;
const doc = await this.insert(tab);
if (this.isSixCron(doc) || doc.extra_schedules?.length) {
if (this.isNodeCron(doc)) {
await cronClient.addCron([
{ name: doc.name || '', id: String(doc.id), schedule: doc.schedule!, command: this.makeCommand(doc), extraSchedules: doc.extra_schedules || [] },
{
name: doc.name || '',
id: String(doc.id),
schedule: doc.schedule!,
command: this.makeCommand(doc),
extraSchedules: doc.extra_schedules || [],
},
]);
}
await this.set_crontab();
@@ -54,17 +65,17 @@ export default class CronService {
if (doc.isDisabled === 1) {
return newDoc;
}
if (this.isSixCron(doc) || doc.extra_schedules?.length) {
if (this.isNodeCron(doc)) {
await cronClient.delCron([String(doc.id)]);
}
if (this.isSixCron(newDoc) || newDoc.extra_schedules?.length) {
if (this.isNodeCron(newDoc)) {
await cronClient.addCron([
{
name: doc.name || '',
id: String(newDoc.id),
schedule: newDoc.schedule!,
command: this.makeCommand(newDoc),
extraSchedules: newDoc.extra_schedules || []
extraSchedules: newDoc.extra_schedules || [],
},
]);
}
@@ -107,7 +118,10 @@ export default class CronService {
if (status === CrontabStatus.idle && log_path !== cron.log_path) {
options = omit(options, ['status', 'log_path', 'pid']);
}
await CrontabModel.update({ ...pickBy(options, (v) => v === 0 || !!v) }, { where: { id } });
await CrontabModel.update(
{ ...pickBy(options, (v) => v === 0 || !!v) },
{ where: { id } },
);
}
}
@@ -396,35 +410,45 @@ export default class CronService {
return taskLimit.runWithCronLimit(() => {
return new Promise(async (resolve: any) => {
const cron = await this.getDb({ id: cronId });
const params = { name: cron.name, command: cron.command, schedule: cron.schedule, extraSchedules: cron.extra_schedules };
const params = {
name: cron.name,
command: cron.command,
schedule: cron.schedule,
extraSchedules: cron.extra_schedules,
};
if (cron.status !== CrontabStatus.queued) {
resolve(params);
return;
}
this.logger.info(`[panel][开始执行任务] 参数 ${JSON.stringify(params)}`);
this.logger.info(
`[panel][开始执行任务] 参数 ${JSON.stringify(params)}`,
);
let { id, command, log_path } = cron;
const uniqPath = await getUniqPath(command, `${id}`);
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
if (log_path?.split('/')?.every(x => x !== uniqPath)) {
fs.mkdirSync(logDirPath, { recursive: true });
if (log_path?.split('/')?.every((x) => x !== uniqPath)) {
await fs.mkdir(logDirPath, { recursive: true });
}
const logPath = `${uniqPath}/${logTime}.log`;
const absolutePath = path.resolve(config.logPath, `${logPath}`);
const cp = spawn(`real_log_path=${logPath} no_delay=true ${this.makeCommand(cron)}`, { shell: '/bin/bash' });
const cp = spawn(
`real_log_path=${logPath} no_delay=true ${this.makeCommand(cron)}`,
{ shell: '/bin/bash' },
);
await CrontabModel.update(
{ status: CrontabStatus.running, pid: cp.pid, log_path: logPath },
{ where: { id } },
);
cp.stderr.on('data', (data) => {
fs.appendFileSync(`${absolutePath}`, `${data.toString()}`);
cp.stderr.on('data', async (data) => {
await fs.appendFile(`${absolutePath}`, `${data.toString()}`);
});
cp.on('error', (err) => {
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
cp.on('error', async (err) => {
await fs.appendFile(`${absolutePath}`, `${JSON.stringify(err)}`);
});
cp.on('exit', async (code) => {
@@ -448,13 +472,13 @@ export default class CronService {
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
const docs = await CrontabModel.findAll({ where: { id: ids } });
const sixCron = docs
.filter((x) => this.isSixCron(x))
.filter((x) => this.isNodeCron(x))
.map((doc) => ({
name: doc.name || '',
id: String(doc.id),
schedule: doc.schedule!,
command: this.makeCommand(doc),
extraSchedules: doc.extra_schedules || []
extraSchedules: doc.extra_schedules || [],
}));
await cronClient.addCron(sixCron);
await this.set_crontab();
@@ -469,7 +493,7 @@ export default class CronService {
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
const logFileExist = doc.log_path && (await fileExist(absolutePath));
if (logFileExist) {
return getFileContentByName(`${absolutePath}`);
return await getFileContentByName(`${absolutePath}`);
} else {
return '任务未运行';
}
@@ -483,15 +507,18 @@ export default class CronService {
const relativeDir = path.dirname(`${doc.log_path}`);
const dir = path.resolve(config.logPath, relativeDir);
if (existsSync(dir)) {
let files = await promises.readdir(dir);
return files
.map((x) => ({
filename: x,
directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
const dirExist = await fileExist(dir);
if (dirExist) {
let files = await fs.readdir(dir);
return (
await Promise.all(
files.map(async (x) => ({
filename: x,
directory: relativeDir.replace(config.logPath, ''),
time: (await fs.stat(`${dir}/${x}`)).mtime.getTime(),
})),
)
).sort((a, b) => b.time - a.time);
} else {
return [];
}
@@ -502,13 +529,15 @@ export default class CronService {
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
command = `${TASK_PREFIX}${tab.command}`;
}
let commandVariable = `no_tee=true ID=${tab.id} `
let commandVariable = `no_tee=true ID=${tab.id} `;
if (tab.task_before) {
commandVariable += `task_before='${tab.task_before.replace(/'/g, "'\\''")
commandVariable += `task_before='${tab.task_before
.replace(/'/g, "'\\''")
.trim()}' `;
}
if (tab.task_after) {
commandVariable += `task_after='${tab.task_after.replace(/'/g, "'\\''")
commandVariable += `task_after='${tab.task_after
.replace(/'/g, "'\\''")
.trim()}' `;
}
@@ -521,7 +550,11 @@ export default class CronService {
var crontab_string = '';
tabs.data.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(/ +/);
if (tab.isDisabled === 1 || _schedule!.length !== 5 || tab.extra_schedules?.length) {
if (
tab.isDisabled === 1 ||
_schedule!.length !== 5 ||
tab.extra_schedules?.length
) {
crontab_string += '# ';
crontab_string += tab.schedule;
crontab_string += ' ';
@@ -535,7 +568,7 @@ export default class CronService {
}
});
fs.writeFileSync(config.crontabFile, crontab_string);
await fs.writeFile(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`);
await CrontabModel.update({ saved: true }, { where: {} });
@@ -580,13 +613,13 @@ export default class CronService {
this.set_crontab(tabs);
const sixCron = tabs.data
.filter((x) => this.isSixCron(x) && x.isDisabled !== 1)
.filter((x) => this.isNodeCron(x) && x.isDisabled !== 1)
.map((doc) => ({
name: doc.name || '',
id: String(doc.id),
schedule: doc.schedule!,
command: this.makeCommand(doc),
extraSchedules: doc.extra_schedules || []
extraSchedules: doc.extra_schedules || [],
}));
await cronClient.addCron(sixCron);
}
+90 -18
View File
@@ -14,7 +14,12 @@ import {
import { spawn } from 'cross-spawn';
import SockService from './sock';
import { FindOptions, Op } from 'sequelize';
import { promiseExecSuccess } from '../config/util';
import {
fileExist,
getPid,
killTask,
promiseExecSuccess,
} from '../config/util';
import dayjs from 'dayjs';
import taskLimit from '../shared/pLimit';
@@ -61,12 +66,23 @@ export default class DependenceService {
}
public async remove(ids: number[], force = false): Promise<Dependence[]> {
await DependenceModel.update(
{ status: DependenceStatus.queued, log: [] },
{ where: { id: ids } },
);
const docs = await DependenceModel.findAll({ where: { id: ids } });
this.installDependenceOneByOne(docs, false, force);
const unInstalledDeps = docs.filter(
(x) => x.status !== DependenceStatus.installed,
);
const installedDeps = docs.filter(
(x) => x.status === DependenceStatus.installed,
);
await this.removeDb(unInstalledDeps.map((x) => x.id!));
if (installedDeps.length) {
await DependenceModel.update(
{ status: DependenceStatus.queued, log: [] },
{ where: { id: ids } },
);
this.installDependenceOneByOne(docs, false, force);
}
return docs;
}
@@ -75,11 +91,21 @@ export default class DependenceService {
}
public async dependencies(
{ searchValue, type }: { searchValue: string; type: string },
sort: any = { position: -1 },
{
searchValue,
type,
status,
}: { searchValue: string; type: string; status: string },
sort: any = [],
query: any = {},
): Promise<Dependence[]> {
let condition = { ...query, type: DependenceTypes[type as any] };
let condition = {
...query,
type: DependenceTypes[type as any],
};
if (status) {
condition.status = status.split(',').map(Number);
}
if (searchValue) {
const encodeText = encodeURI(searchValue);
const reg = {
@@ -95,7 +121,7 @@ export default class DependenceService {
};
}
try {
const result = await this.find(condition);
const result = await this.find(condition, sort);
return result as any;
} catch (error) {
throw error;
@@ -119,10 +145,32 @@ export default class DependenceService {
);
const docs = await DependenceModel.findAll({ where: { id: ids } });
this.installDependenceOneByOne(docs);
this.installDependenceOneByOne(docs, true, true);
return docs;
}
public async cancel(ids: number[]) {
const docs = await DependenceModel.findAll({ where: { id: ids } });
for (const doc of docs) {
taskLimit.removeQueuedDependency(doc);
const depInstallCommand = InstallDependenceCommandTypes[doc.type];
const depUnInstallCommand = unInstallDependenceCommandTypes[doc.type];
const installCmd = `${depInstallCommand} ${doc.name.trim()}`;
const unInstallCmd = `${depUnInstallCommand} ${doc.name.trim()}`;
const pids = await Promise.all([
getPid(installCmd),
getPid(unInstallCmd),
]);
for (const pid of pids) {
pid && (await killTask(pid));
}
}
await DependenceModel.update(
{ status: DependenceStatus.cancelled },
{ where: { id: ids } },
);
}
private async find(query: any, sort: any = []): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({
where: { ...query },
@@ -157,8 +205,14 @@ export default class DependenceService {
isInstall: boolean = true,
force: boolean = false,
) {
return taskLimit.runOneByOne(() => {
return taskLimit.runDependeny(dependency, () => {
return new Promise(async (resolve) => {
if (taskLimit.firstDependencyId !== dependency.id) {
return resolve(null);
}
taskLimit.removeQueuedDependency(dependency);
const depIds = [dependency.id!];
const status = isInstall
? DependenceStatus.installing
@@ -188,7 +242,7 @@ export default class DependenceService {
this.updateLog(depIds, message);
// 判断是否已经安装过依赖
if (isInstall) {
if (isInstall && !force) {
const getCommandPrefix = GetDependenceCommandTypes[dependency.type];
const depVersionStr = versionDependenceCommandTypes[dependency.type];
let depVersion = '';
@@ -241,10 +295,18 @@ export default class DependenceService {
return resolve(null);
}
}
const cp = spawn(`${depRunCommand} ${dependency.name.trim()}`, {
shell: '/bin/bash',
});
const dependenceProxyFileExist = await fileExist(
config.dependenceProxyFile,
);
const proxyStr = dependenceProxyFileExist
? `source ${config.dependenceProxyFile} &&`
: '';
const cp = spawn(
`${proxyStr} ${depRunCommand} ${dependency.name.trim()}`,
{
shell: '/bin/bash',
},
);
cp.stdout.on('data', async (data) => {
this.sockService.sendMessage({
@@ -298,7 +360,17 @@ export default class DependenceService {
? DependenceStatus.installFailed
: DependenceStatus.removeFailed;
}
await DependenceModel.update({ status }, { where: { id: depIds } });
const docs = await DependenceModel.findAll({ where: { id: depIds } });
const _docIds = docs
.filter((x) => x.status !== DependenceStatus.cancelled)
.map((x) => x.id!);
if (_docIds.length > 0) {
await DependenceModel.update(
{ status },
{ where: { id: _docIds } },
);
}
// 如果删除依赖成功或者强制删除
if ((isSucceed || force) && !isInstall) {
+2 -2
View File
@@ -1,7 +1,7 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import * as fs from 'fs';
import * as fs from 'fs/promises';
import {
Env,
EnvModel,
@@ -208,6 +208,6 @@ export default class EnvService {
}
}
}
fs.writeFileSync(config.envFile, env_string);
await fs.writeFile(config.envFile, env_string);
}
}
+11 -8
View File
@@ -57,7 +57,7 @@ export default class NotificationService {
try {
return await notificationModeAction?.call(this);
} catch (error: any) {
return false;
throw error;
}
}
return false;
@@ -234,8 +234,8 @@ export default class NotificationService {
telegramBotUserId,
} = this.params;
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
const url = `https://${
telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
const url = `${
telegramBotApiHost ? telegramBotApiHost : 'https://api.telegram.org'
}/bot${telegramBotToken}/sendMessage`;
let agent;
if (telegramBotProxyHost && telegramBotProxyPort) {
@@ -562,7 +562,7 @@ export default class NotificationService {
throw new Error(JSON.stringify(info));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
throw error;
}
}
@@ -591,7 +591,7 @@ export default class NotificationService {
}
private async chronocat() {
const { chronocatURL, chronocatQQ, chronocatToekn } = this.params;
const { chronocatURL, chronocatQQ, chronocatToken } = this.params;
try {
const user_ids = chronocatQQ
.match(/user_id=(\d+)/g)
@@ -603,7 +603,7 @@ export default class NotificationService {
const url = `${chronocatURL}/api/message/send`;
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${chronocatToekn}`,
Authorization: `Bearer ${chronocatToken}`,
};
for (const [chat_type, ids] of [
@@ -634,7 +634,7 @@ export default class NotificationService {
json: data,
headers,
});
if (res.body === 'success') {
if (res.statusCode === 200) {
return true;
} else {
throw new Error(res.body);
@@ -660,9 +660,11 @@ export default class NotificationService {
webhookUrl,
webhookBody,
);
if (!formatUrl && !formatBody) {
return false;
throw new Error('Url 或者 Body 中必须包含 $title');
}
const headers = parseHeaders(webhookHeaders);
const body = parseBody(formatBody, webhookContentType);
const bodyParam = this.formatBody(webhookContentType, body);
@@ -693,6 +695,7 @@ export default class NotificationService {
case 'multipart/form-data':
return { form: body };
case 'application/x-www-form-urlencoded':
case 'text/plain':
return { body };
}
return {};
+13 -10
View File
@@ -1,13 +1,12 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import fs from 'fs';
import path from 'path';
import path, { join } from 'path';
import SockService from './sock';
import CronService from './cron';
import ScheduleService, { TaskCallbacks } from './schedule';
import config from '../config';
import { TASK_COMMAND } from '../config/const';
import { getPid, killTask } from '../config/util';
import { getFileContentByName, getPid, killTask, rmPath } from '../config/util';
@Service()
export default class ScriptService {
@@ -16,14 +15,12 @@ export default class ScriptService {
private sockService: SockService,
private cronService: CronService,
private scheduleService: ScheduleService,
) { }
) {}
private taskCallbacks(filePath: string): TaskCallbacks {
return {
onEnd: async (cp, endTime, diff) => {
try {
fs.unlinkSync(filePath);
} catch (error) { }
await rmPath(filePath);
},
onError: async (message: string) => {
this.sockService.sendMessage({
@@ -42,7 +39,7 @@ export default class ScriptService {
public async runScript(filePath: string) {
const relativePath = path.relative(config.scriptPath, filePath);
const command = `${TASK_COMMAND} -l ${relativePath} now`;
const command = `${TASK_COMMAND} ${relativePath} now`;
const pid = await this.scheduleService.runTask(
command,
this.taskCallbacks(filePath),
@@ -56,12 +53,18 @@ export default class ScriptService {
public async stopScript(filePath: string, pid: number) {
if (!pid) {
const relativePath = path.relative(config.scriptPath, filePath);
pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`) as number;
pid = (await getPid(`${TASK_COMMAND} ${relativePath} now`)) as number;
}
try {
await killTask(pid);
} catch (error) { }
} catch (error) {}
return { code: 200 };
}
public async getFile(filePath: string, fileName: string) {
const _filePath = join(config.scriptPath, filePath, fileName);
const content = await getFileContentByName(_filePath);
return content;
}
}
+41 -32
View File
@@ -1,11 +1,12 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import fs, { existsSync } from 'fs';
import fs from 'fs/promises';
import os from 'os';
import path from 'path';
import { Subscription } from '../data/subscription';
import { formatUrl } from '../config/subscription';
import config from '../config';
import { fileExist, rmPath } from '../config/util';
@Service()
export default class SshKeyService {
@@ -18,15 +19,16 @@ export default class SshKeyService {
this.initSshConfigFile();
}
private initSshConfigFile() {
private async initSshConfigFile() {
let config = '';
if (existsSync(this.sshConfigFilePath)) {
config = fs.readFileSync(this.sshConfigFilePath, { encoding: 'utf-8' });
const _exist = await fileExist(this.sshConfigFilePath);
if (_exist) {
config = await fs.readFile(this.sshConfigFilePath, { encoding: 'utf-8' });
} else {
fs.writeFileSync(this.sshConfigFilePath, '');
await fs.writeFile(this.sshConfigFilePath, '');
}
if (!config.includes(this.sshConfigHeader)) {
fs.writeFileSync(
await fs.writeFile(
this.sshConfigFilePath,
`${this.sshConfigHeader}\n\n${config}`,
{ encoding: 'utf-8' },
@@ -34,9 +36,12 @@ export default class SshKeyService {
}
}
private generatePrivateKeyFile(alias: string, key: string): void {
private async generatePrivateKeyFile(
alias: string,
key: string,
): Promise<void> {
try {
fs.writeFileSync(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
await fs.writeFile(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
encoding: 'utf8',
mode: '400',
});
@@ -45,18 +50,20 @@ export default class SshKeyService {
}
}
private removePrivateKeyFile(alias: string): void {
private async removePrivateKeyFile(alias: string): Promise<void> {
try {
const filePath = path.join(this.sshPath, alias);
if (existsSync(filePath)) {
fs.unlinkSync(filePath);
}
await rmPath(filePath);
} catch (error) {
this.logger.error('删除私钥文件失败', error);
}
}
private generateSingleSshConfig(alias: string, host: string, proxy?: string) {
private async generateSingleSshConfig(
alias: string,
host: string,
proxy?: string,
) {
if (host === 'github.com') {
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa`;
}
@@ -67,49 +74,51 @@ export default class SshKeyService {
this.sshPath,
alias,
)}\n StrictHostKeyChecking no\n${proxyStr}`;
fs.writeFileSync(`${path.join(this.sshPath, `${alias}.config`)}`, config, {
encoding: 'utf8',
});
await fs.writeFile(
`${path.join(this.sshPath, `${alias}.config`)}`,
config,
{
encoding: 'utf8',
},
);
}
private removeSshConfig(alias: string) {
private async removeSshConfig(alias: string) {
try {
const filePath = path.join(this.sshPath, `${alias}.config`);
if (existsSync(filePath)) {
fs.unlinkSync(filePath);
}
await rmPath(filePath);
} catch (error) {
this.logger.error(`删除ssh配置文件${alias}失败`, error);
}
}
public addSSHKey(
public async addSSHKey(
key: string,
alias: string,
host: string,
proxy?: string,
): void {
this.generatePrivateKeyFile(alias, key);
this.generateSingleSshConfig(alias, host, proxy);
): Promise<void> {
await this.generatePrivateKeyFile(alias, key);
await this.generateSingleSshConfig(alias, host, proxy);
}
public removeSSHKey(alias: string, host: string, proxy?: string): void {
this.removePrivateKeyFile(alias);
this.removeSshConfig(alias);
public async removeSSHKey(alias: string, host: string, proxy?: string): Promise<void> {
await this.removePrivateKeyFile(alias);
await this.removeSshConfig(alias);
}
public setSshConfig(docs: Subscription[]) {
public async setSshConfig(docs: Subscription[]) {
for (const doc of docs) {
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
const { alias, proxy } = doc;
const { host } = formatUrl(doc);
this.removePrivateKeyFile(alias);
this.removeSshConfig(alias);
this.generatePrivateKeyFile(
await this.removePrivateKeyFile(alias);
await this.removeSshConfig(alias);
await this.generatePrivateKeyFile(
alias,
(doc.pull_option as any).private_key,
);
this.generateSingleSshConfig(alias, host, proxy);
await this.generateSingleSshConfig(alias, host, proxy);
}
}
}
+27 -25
View File
@@ -7,7 +7,6 @@ import {
SubscriptionStatus,
} from '../data/subscription';
import { ChildProcessWithoutNullStreams } from 'child_process';
import fs from 'fs';
import {
getFileContentByName,
concurrentRun,
@@ -16,9 +15,9 @@ import {
killTask,
handleLogPath,
promiseExec,
emptyDir,
rmPath,
} from '../config/util';
import { promises, existsSync } from 'fs';
import fs from 'fs/promises';
import { FindOptions, Op } from 'sequelize';
import path, { join } from 'path';
import ScheduleService, { TaskCallbacks } from './schedule';
@@ -39,7 +38,7 @@ export default class SubscriptionService {
private sockService: SockService,
private sshKeyService: SshKeyService,
private crontabService: CrontabService,
) { }
) {}
public async list(searchText?: string): Promise<Subscription[]> {
let query = {};
@@ -107,7 +106,7 @@ export default class SubscriptionService {
public async setSshConfig() {
const docs = await SubscriptionModel.findAll();
this.sshKeyService.setSshConfig(docs);
await this.sshKeyService.setSshConfig(docs);
}
private taskCallbacks(doc: Subscription): TaskCallbacks {
@@ -131,7 +130,7 @@ export default class SubscriptionService {
let beforeStr = '';
try {
if (doc.sub_before) {
fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`);
await fs.appendFile(absolutePath, `\n## 执行before命令...\n\n`);
beforeStr = await promiseExec(doc.sub_before);
}
} catch (error: any) {
@@ -139,7 +138,7 @@ export default class SubscriptionService {
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
}
if (beforeStr) {
fs.appendFileSync(absolutePath, `${beforeStr}\n`);
await fs.appendFile(absolutePath, `${beforeStr}\n`);
}
},
onStart: async (cp: ChildProcessWithoutNullStreams, startTime) => {
@@ -158,7 +157,7 @@ export default class SubscriptionService {
let afterStr = '';
try {
if (sub.sub_after) {
fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n\n`);
await fs.appendFile(absolutePath, `\n\n## 执行after命令...\n\n`);
afterStr = await promiseExec(sub.sub_after);
}
} catch (error: any) {
@@ -166,10 +165,10 @@ export default class SubscriptionService {
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
}
if (afterStr) {
fs.appendFileSync(absolutePath, `${afterStr}\n`);
await fs.appendFile(absolutePath, `${afterStr}\n`);
}
fs.appendFileSync(
await fs.appendFile(
absolutePath,
`\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
@@ -190,12 +189,12 @@ export default class SubscriptionService {
onError: async (message: string) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await handleLogPath(sub.log_path as string);
fs.appendFileSync(absolutePath, `\n${message}`);
await fs.appendFile(absolutePath, `\n${message}`);
},
onLog: async (message: string) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await handleLogPath(sub.log_path as string);
fs.appendFileSync(absolutePath, `\n${message}`);
await fs.appendFile(absolutePath, `\n${message}`);
},
};
}
@@ -268,11 +267,11 @@ export default class SubscriptionService {
if (query?.force === true) {
const crons = await CrontabModel.findAll({ where: { sub_id: ids } });
if (crons?.length) {
await this.crontabService.remove(crons.map(x => x.id!))
await this.crontabService.remove(crons.map((x) => x.id!));
}
for (const doc of docs) {
const filePath = join(config.scriptPath, doc.alias);
emptyDir(filePath);
await rmPath(filePath);
}
}
}
@@ -323,7 +322,7 @@ export default class SubscriptionService {
this.scheduleService.runTask(command, this.taskCallbacks(subscription), {
name: subscription.name,
schedule: subscription.schedule,
command
command,
});
}
@@ -352,7 +351,7 @@ export default class SubscriptionService {
}
const absolutePath = await handleLogPath(doc.log_path as string);
return getFileContentByName(absolutePath);
return await getFileContentByName(absolutePath);
}
public async logs(id: number) {
@@ -364,15 +363,18 @@ export default class SubscriptionService {
if (doc.log_path) {
const relativeDir = path.dirname(`${doc.log_path}`);
const dir = path.resolve(config.logPath, relativeDir);
if (existsSync(dir)) {
let files = await promises.readdir(dir);
return files
.map((x) => ({
filename: x,
directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
const _exist = await fileExist(dir);
if (_exist) {
let files = await fs.readdir(dir);
return (
await Promise.all(
files.map(async (x) => ({
filename: x,
directory: relativeDir.replace(config.logPath, ''),
time: (await fs.stat(`${dir}/${x}`)).mtime.getTime(),
})),
)
).sort((a, b) => b.time - a.time);
}
}
}
+170 -29
View File
@@ -5,10 +5,10 @@ import config from '../config';
import {
AuthDataType,
AuthInfo,
AuthInstance,
AuthModel,
AuthModelInfo,
} from '../data/auth';
SystemInstance,
SystemModel,
SystemModelInfo,
} from '../data/system';
import { NotificationInfo } from '../data/notify';
import NotificationService from './notify';
import ScheduleService, { TaskCallbacks } from './schedule';
@@ -29,6 +29,7 @@ import tar from 'tar';
import path from 'path';
import fs from 'fs';
import sum from 'lodash/sum';
import { DependenceModel, DependenceStatus, DependenceTypes } from '../data/dependence';
@Service()
export default class SystemService {
@@ -39,21 +40,21 @@ export default class SystemService {
@Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService,
private sockService: SockService,
) { }
) {}
public async getSystemConfig() {
const doc = await this.getDb({ type: AuthDataType.systemConfig });
return doc || ({} as AuthInstance);
return doc || ({} as SystemInstance);
}
private async updateAuthDb(payload: AuthInfo): Promise<AuthInstance> {
await AuthModel.upsert({ ...payload });
private async updateAuthDb(payload: AuthInfo): Promise<SystemInstance> {
await SystemModel.upsert({ ...payload });
const doc = await this.getDb({ type: payload.type });
return doc;
}
public async getDb(query: any): Promise<AuthInstance> {
const doc: any = await AuthModel.findOne({ where: { ...query } });
public async getDb(query: any): Promise<SystemInstance> {
const doc: any = await SystemModel.findOne({ where: { ...query } });
return doc && doc.get({ plain: true });
}
@@ -75,32 +76,171 @@ export default class SystemService {
}
}
public async updateSystemConfig(info: AuthModelInfo) {
public async updateLogRemoveFrequency(info: SystemModelInfo) {
const oDoc = await this.getSystemConfig();
const result = await this.updateAuthDb({
...oDoc,
type: AuthDataType.systemConfig,
info,
info: { ...oDoc.info, ...info },
});
if (info.logRemoveFrequency) {
const cron = {
id: result.id || NaN,
name: '删除日志',
command: `ql rmlog ${info.logRemoveFrequency}`,
};
const cron = {
id: result.id || NaN,
name: '删除日志',
command: `ql rmlog ${info.logRemoveFrequency}`,
};
if (oDoc.info?.logRemoveFrequency) {
await this.scheduleService.cancelIntervalTask(cron);
if (info.logRemoveFrequency > 0) {
this.scheduleService.createIntervalTask(cron, {
days: info.logRemoveFrequency,
});
}
}
if (info.logRemoveFrequency && info.logRemoveFrequency > 0) {
this.scheduleService.createIntervalTask(cron, {
days: info.logRemoveFrequency,
});
}
return { code: 200, data: info };
}
public async updateCronConcurrency(info: SystemModelInfo) {
const oDoc = await this.getSystemConfig();
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
if (info.cronConcurrency) {
await taskLimit.setCustomLimit(info.cronConcurrency);
}
return { code: 200, data: info };
}
public async updateDependenceProxy(info: SystemModelInfo) {
const oDoc = await this.getSystemConfig();
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
if (info.dependenceProxy) {
await fs.promises.writeFile(
config.dependenceProxyFile,
`export http_proxy="${info.dependenceProxy}"\nexport https_proxy="${info.dependenceProxy}"`,
);
} else {
await fs.promises.rm(config.dependenceProxyFile);
}
return { code: 200, data: info };
}
public async updateNodeMirror(info: SystemModelInfo, res?: Response) {
const oDoc = await this.getSystemConfig();
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
let cmd = 'pnpm config delete registry';
if (info.nodeMirror) {
cmd = `pnpm config set registry ${info.nodeMirror}`;
}
let command = `cd && ${cmd}`;
const docs = await DependenceModel.findAll({
where: { type: DependenceTypes.nodejs, status: DependenceStatus.installed },
});
if (docs.length > 0) {
command += ` && pnpm i -g`;
}
this.scheduleService.runTask(
command,
{
onStart: async (cp) => {
res?.setHeader('QL-Task-Pid', `${cp.pid}`);
res?.end();
},
onEnd: async () => {
this.sockService.sendMessage({
type: 'updateNodeMirror',
message: 'update node mirror end',
});
},
onError: async (message: string) => {
this.sockService.sendMessage({ type: 'updateNodeMirror', message });
},
onLog: async (message: string) => {
this.sockService.sendMessage({ type: 'updateNodeMirror', message });
},
},
{
command,
},
);
}
public async updatePythonMirror(info: SystemModelInfo) {
const oDoc = await this.getSystemConfig();
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
let cmd = 'pip config unset global.index-url';
if (info.pythonMirror) {
cmd = `pip3 config set global.index-url ${info.pythonMirror}`;
}
await promiseExec(cmd);
return { code: 200, data: info };
}
public async updateLinuxMirror(
info: SystemModelInfo,
res?: Response,
onEnd?: () => void,
) {
const oDoc = await this.getSystemConfig();
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
let defaultDomain = 'https://dl-cdn.alpinelinux.org';
let targetDomain = 'https://dl-cdn.alpinelinux.org';
const content = await fs.promises.readFile('/etc/apk/repositories', {
encoding: 'utf-8',
});
const domainMatch = content.match(/(http.*)\/alpine\/.*/);
if (domainMatch) {
defaultDomain = domainMatch[1];
}
if (info.linuxMirror) {
targetDomain = info.linuxMirror;
}
const command = `sed -i 's/${defaultDomain.replace(
/\//g,
'\\/',
)}/${targetDomain.replace(
/\//g,
'\\/',
)}/g' /etc/apk/repositories && apk update -f`;
this.scheduleService.runTask(
command,
{
onStart: async (cp) => {
res?.setHeader('QL-Task-Pid', `${cp.pid}`);
res?.end();
},
onEnd: async () => {
this.sockService.sendMessage({
type: 'updateLinuxMirror',
message: 'update linux mirror end',
});
onEnd?.();
},
onError: async (message: string) => {
this.sockService.sendMessage({ type: 'updateLinuxMirror', message });
},
onLog: async (message: string) => {
this.sockService.sendMessage({ type: 'updateLinuxMirror', message });
},
},
{
command,
},
);
}
public async checkUpdate() {
try {
const currentVersionContent = await parseVersion(config.versionFile);
@@ -114,7 +254,7 @@ export default class SystemService {
},
);
lastVersionContent = await parseContentVersion(result.body);
} catch (error) { }
} catch (error) {}
if (!lastVersionContent) {
lastVersionContent = currentVersionContent;
@@ -160,7 +300,7 @@ export default class SystemService {
}
public async updateSystem() {
const cp = spawn('ql -l update false', { shell: '/bin/bash' });
const cp = spawn('real_time=true ql update false', { shell: '/bin/bash' });
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
@@ -187,7 +327,8 @@ export default class SystemService {
}
public async reloadSystem(target: 'system' | 'data') {
const cp = spawn(`ql -l reload ${target || ''}`, { shell: '/bin/bash' });
const cmd = `real_time=true ql reload ${target || ''}`;
const cp = spawn(cmd, { shell: '/bin/bash' });
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
@@ -234,7 +375,7 @@ export default class SystemService {
callback,
{
command,
}
},
);
}
@@ -283,7 +424,7 @@ export default class SystemService {
}
public async getSystemLog(res: Response) {
const result = readDirs(config.systemLogPath, config.systemLogPath);
const result = await readDirs(config.systemLogPath, config.systemLogPath);
const logs = result.reverse().filter((x) => x.title.endsWith('.log'));
res.set({
'Content-Length': sum(logs.map((x) => x.size)),
+48 -37
View File
@@ -8,22 +8,25 @@ import {
safeJSONParse,
} from '../config/util';
import config from '../config';
import * as fs from 'fs';
import * as fs from 'fs/promises';
import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default';
import {
AuthDataType,
AuthInfo,
AuthModel,
AuthModelInfo,
SystemModel,
SystemModelInfo,
LoginStatus,
} from '../data/auth';
} from '../data/system';
import { NotificationInfo } from '../data/notify';
import NotificationService from './notify';
import { Request } from 'express';
import ScheduleService from './schedule';
import SockService from './sock';
import dayjs from 'dayjs';
import IP2Region from 'ip2region';
import requestIp from 'request-ip';
import uniq from 'lodash/uniq';
@Service()
export default class UserService {
@@ -44,12 +47,13 @@ export default class UserService {
req: Request,
needTwoFactor = true,
): Promise<any> {
if (!fs.existsSync(config.authConfigFile)) {
const _exist = await fileExist(config.authConfigFile);
if (!_exist) {
return this.initAuthInfo();
}
let { username, password } = payloads;
const content = this.getAuthInfo();
const content = await this.getAuthInfo();
const timestamp = Date.now();
if (content) {
let {
@@ -102,7 +106,16 @@ export default class UserService {
};
}
const { ip, address } = await getNetIp(req);
const ip = requestIp.getClientIp(req) || '';
const query = new IP2Region();
const ipAddress = query.search(ip);
let address = '';
if (ipAddress) {
const { country, province, city, isp } = ipAddress;
address = uniq([country, province, city, isp])
.filter(Boolean)
.join(' ');
}
if (username === cUsername && password === cPassword) {
const data = createRandomString(50, 100);
const expiration = twoFactorActivated ? 60 : 20;
@@ -124,13 +137,12 @@ export default class UserService {
platform: req.platform,
isTwoFactorChecking: false,
});
await this.notificationService.notify(
this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
}端 登录成功,ip地址 ${ip}`,
);
await this.getLoginLog();
await this.insertDb({
type: AuthDataType.loginLog,
info: {
@@ -153,13 +165,12 @@ export default class UserService {
lastaddr: address,
platform: req.platform,
});
await this.notificationService.notify(
this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
}端 登录失败,ip地址 ${ip}`,
);
await this.getLoginLog();
await this.insertDb({
type: AuthDataType.loginLog,
info: {
@@ -187,15 +198,15 @@ export default class UserService {
}
public async logout(platform: string): Promise<any> {
const authInfo = this.getAuthInfo();
const authInfo = await this.getAuthInfo();
this.updateAuthInfo(authInfo, {
token: '',
tokens: { ...authInfo.tokens, [platform]: '' },
});
}
public async getLoginLog(): Promise<Array<AuthModelInfo | undefined>> {
const docs = await AuthModel.findAll({
public async getLoginLog(): Promise<Array<SystemModelInfo | undefined>> {
const docs = await SystemModel.findAll({
where: { type: AuthDataType.loginLog },
});
if (docs && docs.length > 0) {
@@ -203,7 +214,7 @@ export default class UserService {
(a, b) => b.info!.timestamp! - a.info!.timestamp!,
);
if (result.length > 100) {
await AuthModel.destroy({
await SystemModel.destroy({
where: { id: result[result.length - 1].id },
});
}
@@ -213,12 +224,12 @@ export default class UserService {
}
private async insertDb(payload: AuthInfo): Promise<AuthInfo> {
const doc = await AuthModel.create({ ...payload }, { returning: true });
const doc = await SystemModel.create({ ...payload }, { returning: true });
return doc;
}
private initAuthInfo() {
fs.writeFileSync(
private async initAuthInfo() {
await fs.writeFile(
config.authConfigFile,
JSON.stringify({
username: 'admin',
@@ -241,13 +252,13 @@ export default class UserService {
if (password === 'admin') {
return { code: 400, message: '密码不能设置为admin' };
}
const authInfo = this.getAuthInfo();
const authInfo = await this.getAuthInfo();
this.updateAuthInfo(authInfo, { username, password });
return { code: 200, message: '更新成功' };
}
public async updateAvatar(avatar: string) {
const authInfo = this.getAuthInfo();
const authInfo = await this.getAuthInfo();
this.updateAuthInfo(authInfo, { avatar });
return { code: 200, data: avatar, message: '更新成功' };
}
@@ -255,7 +266,7 @@ export default class UserService {
public async getUserInfo(): Promise<any> {
const authFileExist = await fileExist(config.authConfigFile);
if (!authFileExist) {
fs.writeFileSync(
await fs.writeFile(
config.authConfigFile,
JSON.stringify({
username: 'admin',
@@ -263,19 +274,19 @@ export default class UserService {
}),
);
}
return this.getAuthInfo();
return await this.getAuthInfo();
}
public initTwoFactor() {
public async initTwoFactor() {
const secret = authenticator.generateSecret();
const authInfo = this.getAuthInfo();
const authInfo = await this.getAuthInfo();
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
return { secret, url: otpauth };
}
public activeTwoFactor(code: string) {
const authInfo = this.getAuthInfo();
public async activeTwoFactor(code: string) {
const authInfo = await this.getAuthInfo();
const isValid = authenticator.verify({
token: code,
secret: authInfo.twoFactorSecret,
@@ -294,7 +305,7 @@ export default class UserService {
}: { username: string; password: string; code: string },
req: any,
) {
const authInfo = this.getAuthInfo();
const authInfo = await this.getAuthInfo();
const { isTwoFactorChecking, twoFactorSecret } = authInfo;
if (!isTwoFactorChecking) {
return { code: 450, message: '未知错误' };
@@ -316,8 +327,8 @@ export default class UserService {
}
}
public deactiveTwoFactor() {
const authInfo = this.getAuthInfo();
public async deactiveTwoFactor() {
const authInfo = await this.getAuthInfo();
this.updateAuthInfo(authInfo, {
twoFactorActivated: false,
twoFactorActived: false,
@@ -326,13 +337,13 @@ export default class UserService {
return true;
}
private getAuthInfo() {
const content = fs.readFileSync(config.authConfigFile, 'utf8');
private async getAuthInfo() {
const content = await fs.readFile(config.authConfigFile, 'utf8');
return safeJSONParse(content);
}
private updateAuthInfo(authInfo: any, info: any) {
fs.writeFileSync(
private async updateAuthInfo(authInfo: any, info: any) {
await fs.writeFile(
config.authConfigFile,
JSON.stringify({ ...authInfo, ...info }),
);
@@ -344,21 +355,21 @@ export default class UserService {
}
private async updateAuthDb(payload: AuthInfo): Promise<any> {
let doc = await AuthModel.findOne({ type: payload.type });
let doc = await SystemModel.findOne({ type: payload.type });
if (doc) {
const updateResult = await AuthModel.update(payload, {
const updateResult = await SystemModel.update(payload, {
where: { id: doc.id },
returning: true,
});
doc = updateResult[1][0];
} else {
doc = await AuthModel.create(payload, { returning: true });
doc = await SystemModel.create(payload, { returning: true });
}
return doc;
}
public async getDb(query: any): Promise<any> {
const doc: any = await AuthModel.findOne({ where: { ...query } });
const doc: any = await SystemModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as any);
}
+49 -23
View File
@@ -1,12 +1,20 @@
import PQueue, { QueueAddOptions } from 'p-queue-cjs';
import os from 'os';
import { AuthDataType, AuthModel } from '../data/auth';
import { AuthDataType, SystemModel } from '../data/system';
import Logger from '../loaders/logger';
import { Dependence } from '../data/dependence';
interface IDependencyFn<T> {
(): Promise<T>;
dependency?: Dependence;
}
class TaskLimit {
private oneLimit = new PQueue({ concurrency: 1 });
private dependenyLimit = new PQueue({ concurrency: 1 });
private queuedDependencyIds = new Set<number>([]);
private updateLogLimit = new PQueue({ concurrency: 1 });
private cronLimit = new PQueue({ concurrency: Math.max(os.cpus().length, 4) });
private cronLimit = new PQueue({
concurrency: Math.max(os.cpus().length, 4),
});
get cronLimitActiveCount() {
return this.cronLimit.pending;
@@ -16,6 +24,10 @@ class TaskLimit {
return this.cronLimit.size;
}
get firstDependencyId() {
return [...this.queuedDependencyIds.values()][0];
}
constructor() {
this.setCustomLimit();
this.handleEvents();
@@ -26,21 +38,19 @@ class TaskLimit {
Logger.info(
`[schedule][任务加入队列] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,
);
})
});
this.cronLimit.on('active', () => {
Logger.info(
`[schedule][开始处理任务] 运行中任务数: ${this.cronLimitActiveCount + 1}, 等待中任务数: ${this.cronLimitPendingCount}`,
);
})
this.cronLimit.on('completed', (param) => {
Logger.info(
`[schedule][任务处理成功] 参数 ${JSON.stringify(param)}`,
`[schedule][开始处理任务] 运行中任务数: ${
this.cronLimitActiveCount + 1
}, 等待中任务数: ${this.cronLimitPendingCount}`,
);
});
this.cronLimit.on('error', error => {
Logger.error(
`[schedule][任务处理错误] 参数 ${JSON.stringify(error)}`,
);
this.cronLimit.on('completed', (param) => {
Logger.info(`[schedule][任务处理成功] 参数 ${JSON.stringify(param)}`);
});
this.cronLimit.on('error', (error) => {
Logger.error(`[schedule][任务处理错误] 参数 ${JSON.stringify(error)}`);
});
this.cronLimit.on('next', () => {
Logger.info(
@@ -48,19 +58,23 @@ class TaskLimit {
);
});
this.cronLimit.on('idle', () => {
Logger.info(
`[schedule][任务队列] 空闲中...`,
);
Logger.info(`[schedule][任务队列] 空闲中...`);
});
}
public removeQueuedDependency(dependency: Dependence) {
if (this.queuedDependencyIds.has(dependency.id!)) {
this.queuedDependencyIds.delete(dependency.id!);
}
}
public async setCustomLimit(limit?: number) {
if (limit) {
this.cronLimit.concurrency = limit;
return;
}
await AuthModel.sync();
const doc = await AuthModel.findOne({
await SystemModel.sync();
const doc = await SystemModel.findOne({
where: { type: AuthDataType.systemConfig },
});
if (doc?.info?.cronConcurrency) {
@@ -68,15 +82,27 @@ class TaskLimit {
}
}
public async runWithCronLimit<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
public async runWithCronLimit<T>(
fn: () => Promise<T>,
options?: Partial<QueueAddOptions>,
): Promise<T | void> {
return this.cronLimit.add(fn, options);
}
public runOneByOne<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
return this.oneLimit.add(fn, options);
public runDependeny<T>(
dependency: Dependence,
fn: IDependencyFn<T>,
options?: Partial<QueueAddOptions>,
): Promise<T | void> {
this.queuedDependencyIds.add(dependency.id!);
fn.dependency = dependency;
return this.dependenyLimit.add(fn, options);
}
public updateDepLog<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
public updateDepLog<T>(
fn: () => Promise<T>,
options?: Partial<QueueAddOptions>,
): Promise<T | void> {
return this.updateLogLimit.add(fn, options);
}
}
+77
View File
@@ -0,0 +1,77 @@
FROM python:3.10-alpine3.18 as builder
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
RUN set -x \
&& apk update \
&& apk add nodejs npm git \
&& npm i -g pnpm@8.3.1 \
&& cd /tmp/build \
&& pnpm install --prod
FROM python:3.10-alpine3.18
ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}"
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=develop
ENV PNPM_HOME=/root/.local/share/pnpm \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules \
LANG=C.UTF-8 \
SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \
QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH}
RUN set -x \
&& apk update -f \
&& apk upgrade \
&& apk --no-cache add -f bash \
coreutils \
git \
curl \
wget \
tzdata \
perl \
openssl \
nginx \
nodejs \
jq \
openssh \
procps \
netcat-openbsd \
unzip \
npm \
&& rm -rf /var/cache/apk/* \
&& apk update \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \
&& git config --global http.postBuffer 524288000 \
&& npm install -g pnpm@8.3.1 pm2 tsx \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \
&& rm -rf /root/.npm \
&& ulimit -c 0
ARG SOURCE_COMMIT
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& cd ${QL_DIR} \
&& cp -f .env.example .env \
&& chmod 777 ${QL_DIR}/shell/*.sh \
&& chmod 777 ${QL_DIR}/docker/*.sh \
&& git clone --depth=1 -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
&& mkdir -p ${QL_DIR}/static \
&& cp -rf /static/* ${QL_DIR}/static \
&& rm -rf /static
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
WORKDIR ${QL_DIR}
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
CMD curl -sf --noproxy '*' http://127.0.0.1:5400/api/health || exit 1
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+12 -9
View File
@@ -1,13 +1,13 @@
FROM python:3.10-alpine as builder
FROM python:3.11-alpine3.18 as builder
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
RUN set -x \
&& apk update \
&& apk add nodejs npm git \
&& npm i -g pnpm@8.3.1 \
&& cd /tmp/build \
&& pnpm --registry https://registry.npmmirror.com install --prod
&& pnpm install --prod
FROM python:3.10-alpine
FROM python:3.11-alpine3.18
ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}"
@@ -23,8 +23,11 @@ ENV PNPM_HOME=/root/.local/share/pnpm \
QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH}
VOLUME /ql/data
EXPOSE 5700
RUN set -x \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update -f \
&& apk upgrade \
&& apk --no-cache add -f bash \
@@ -41,6 +44,7 @@ RUN set -x \
openssh \
procps \
netcat-openbsd \
unzip \
npm \
&& rm -rf /var/cache/apk/* \
&& apk update \
@@ -49,13 +53,12 @@ RUN set -x \
&& git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \
&& git config --global http.postBuffer 524288000 \
&& npm install -g pnpm@8.3.1 \
&& cd && pnpm config set registry https://registry.npmmirror.com \
&& pnpm add -g pm2 tsx \
&& npm install -g pnpm@8.3.1 pm2 tsx \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \
&& rm -rf /root/.npm
&& rm -rf /root/.npm \
&& ulimit -c 0
ARG SOURCE_COMMIT
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
@@ -73,6 +76,6 @@ COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
WORKDIR ${QL_DIR}
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
CMD curl -sf http://127.0.0.1:5400/api/health || exit 1
CMD curl -sf --noproxy '*' http://127.0.0.1:5400/api/health || exit 1
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+3 -16
View File
@@ -1,6 +1,7 @@
#!/bin/bash
dir_shell=/ql/shell
. $dir_shell/env.sh
. $dir_shell/share.sh
link_shell
@@ -14,20 +15,6 @@ pm2 l &>/dev/null
echo -e "======================2. 安装依赖========================\n"
patch_version
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
current_npm_registry=$(cd && pnpm config get registry)
is_equal_registry=$(echo $current_npm_registry | grep "${NpmMirror}")
if [[ "$is_equal_registry" == "" ]]; then
cd && pnpm config set registry $NpmMirror
pnpm install -g
fi
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
fi
echo
echo -e "======================3. 启动nginx========================\n"
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
@@ -38,13 +25,13 @@ reload_pm2
if [[ $AutoStartBot == true ]]; then
echo -e "======================5. 启动bot========================\n"
nohup ql -l bot >$dir_log/bot.log 2>&1 &
nohup ql bot >$dir_log/bot.log 2>&1 &
echo -e "bot后台启动中...\n"
fi
if [[ $EnableExtraShell == true ]]; then
echo -e "====================6. 执行自定义脚本========================\n"
nohup ql -l extra >$dir_log/extra.log 2>&1 &
nohup ql extra >$dir_log/extra.log 2>&1 &
echo -e "自定义脚本后台执行中...\n"
fi
+12
View File
@@ -22,6 +22,10 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://publicApi/api/;
proxy_buffering off;
proxy_redirect default;
proxy_connect_timeout 1800;
proxy_send_timeout 1800;
proxy_read_timeout 1800;
}
location QL_BASE_URLapi/ {
@@ -30,6 +34,10 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://baseApi/api/;
proxy_buffering off;
proxy_redirect default;
proxy_connect_timeout 1800;
proxy_send_timeout 1800;
proxy_read_timeout 1800;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
@@ -41,6 +49,10 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://baseApi/open/;
proxy_buffering off;
proxy_redirect default;
proxy_connect_timeout 1800;
proxy_send_timeout 1800;
proxy_read_timeout 1800;
}
gzip on;
+8
View File
@@ -9,6 +9,14 @@ module.exports = {
source_map_support: true,
time: true,
script: 'static/build/schedule/index.js',
env: {
http_proxy: '',
https_proxy: '',
HTTP_PROXY: '',
HTTPS_PROXY: '',
all_proxy: '',
ALL_PROXY: '',
},
},
{
name: 'public',
+11 -6
View File
@@ -58,7 +58,6 @@
"@grpc/grpc-js": "^1.8.13",
"@otplib/preset-default": "^12.0.1",
"@sentry/node": "^7.12.1",
"@sentry/tracing": "^7.12.1",
"body-parser": "^1.19.2",
"celebrate": "^15.0.1",
"chokidar": "^3.5.3",
@@ -79,7 +78,7 @@
"js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21",
"multer": "^1.4.4",
"multer": "1.4.5-lts.1",
"nedb": "^1.8.0",
"node-schedule": "^2.1.0",
"nodemailer": "^6.7.2",
@@ -97,7 +96,10 @@
"uuid": "^8.3.2",
"winston": "^3.6.0",
"winston-daily-rotate-file": "^4.7.1",
"yargs": "^17.3.1"
"yargs": "^17.3.1",
"tough-cookie": "^4.0.0",
"request-ip": "3.3.0",
"ip2region": "2.3.0"
},
"devDependencies": {
"@ant-design/icons": "^4.7.0",
@@ -110,7 +112,7 @@
"@types/cross-spawn": "^6.0.2",
"@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4",
"@types/file-saver": "^2.0.5",
"@types/file-saver": "2.0.2",
"@types/js-yaml": "^4.0.5",
"@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.185",
@@ -128,6 +130,7 @@
"@types/sockjs-client": "^1.5.1",
"@types/tar": "^6.1.5",
"@types/uuid": "^8.3.4",
"@types/request-ip": "0.0.41",
"@uiw/codemirror-extensions-langs": "^4.21.9",
"@uiw/react-codemirror": "^4.21.9",
"@umijs/max": "^4.0.72",
@@ -139,11 +142,13 @@
"axios": "^1.4.0",
"compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0",
"file-saver": "^2.0.5",
"react-hotkeys-hook": "^4.4.1",
"file-saver": "2.0.2",
"lint-staged": "^13.0.3",
"monaco-editor": "0.33.0",
"nodemon": "^2.0.15",
"nodemon": "^3.0.1",
"prettier": "^2.5.1",
"pretty-bytes": "6.1.1",
"qiniu": "^7.4.0",
"qrcode.react": "^1.0.1",
"query-string": "^7.1.1",
+170 -77
View File
@@ -10,9 +10,6 @@ dependencies:
'@sentry/node':
specifier: ^7.12.1
version: 7.54.0
'@sentry/tracing':
specifier: ^7.12.1
version: 7.54.0
body-parser:
specifier: ^1.19.2
version: 1.20.2
@@ -64,6 +61,9 @@ dependencies:
iconv-lite:
specifier: ^0.6.3
version: 0.6.3
ip2region:
specifier: 2.3.0
version: 2.3.0(@types/node@17.0.45)
js-yaml:
specifier: ^4.1.0
version: 4.1.0
@@ -74,8 +74,8 @@ dependencies:
specifier: ^4.17.21
version: 4.17.21
multer:
specifier: ^1.4.4
version: 1.4.4
specifier: 1.4.5-lts.1
version: 1.4.5-lts.1
nedb:
specifier: ^1.8.0
version: 1.8.0
@@ -97,6 +97,9 @@ dependencies:
reflect-metadata:
specifier: ^0.1.13
version: 0.1.13
request-ip:
specifier: 3.3.0
version: 3.3.0
sequelize:
specifier: ^6.25.5
version: 6.32.0(@whyour/sqlite3@1.0.3)
@@ -115,6 +118,9 @@ dependencies:
toad-scheduler:
specifier: ^1.6.0
version: 1.6.1
tough-cookie:
specifier: ^4.0.0
version: 4.0.0
typedi:
specifier: ^0.10.0
version: 0.10.0
@@ -163,8 +169,8 @@ devDependencies:
specifier: ^6.0.4
version: 6.0.4
'@types/file-saver':
specifier: ^2.0.5
version: 2.0.5
specifier: 2.0.2
version: 2.0.2
'@types/js-yaml':
specifier: ^4.0.5
version: 4.0.5
@@ -201,6 +207,9 @@ devDependencies:
'@types/react-dom':
specifier: ^18.0.6
version: 18.2.4
'@types/request-ip':
specifier: 0.0.41
version: 0.0.41
'@types/serve-handler':
specifier: ^6.1.1
version: 6.1.1
@@ -250,8 +259,8 @@ devDependencies:
specifier: ^7.0.0
version: 7.6.0
file-saver:
specifier: ^2.0.5
version: 2.0.5
specifier: 2.0.2
version: 2.0.2
lint-staged:
specifier: ^13.0.3
version: 13.2.2
@@ -259,11 +268,14 @@ devDependencies:
specifier: 0.33.0
version: 0.33.0
nodemon:
specifier: ^2.0.15
version: 2.0.22
specifier: ^3.0.1
version: 3.0.1
prettier:
specifier: ^2.5.1
version: 2.8.8
pretty-bytes:
specifier: 6.1.1
version: 6.1.1
qiniu:
specifier: ^7.4.0
version: 7.8.0
@@ -297,6 +309,9 @@ devDependencies:
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
react-hotkeys-hook:
specifier: ^4.4.1
version: 4.4.1(react-dom@18.2.0)(react@18.2.0)
react-intl-universal:
specifier: ^2.6.21
version: 2.6.21(react@18.2.0)
@@ -3694,6 +3709,7 @@ packages:
/@gar/promisify@1.1.3:
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
requiresBuild: true
dev: false
optional: true
@@ -4028,18 +4044,18 @@ packages:
react-is: 16.13.1
dev: true
/@mapbox/node-pre-gyp@1.0.10:
resolution: {integrity: sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==}
/@mapbox/node-pre-gyp@1.0.11:
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
hasBin: true
dependencies:
detect-libc: 2.0.1
https-proxy-agent: 5.0.1
make-dir: 3.1.0
node-fetch: 2.6.11
node-fetch: 2.7.0
nopt: 5.0.0
npmlog: 5.0.1
rimraf: 3.0.2
semver: 7.5.1
semver: 7.5.4
tar: 6.1.15
transitivePeerDependencies:
- encoding
@@ -4128,9 +4144,10 @@ packages:
/@npmcli/fs@1.1.1:
resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==}
requiresBuild: true
dependencies:
'@gar/promisify': 1.1.3
semver: 7.5.1
semver: 7.5.4
dev: false
optional: true
@@ -4138,6 +4155,7 @@ packages:
resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==}
engines: {node: '>=10'}
deprecated: This functionality has been moved to @npmcli/fs
requiresBuild: true
dependencies:
mkdirp: 1.0.4
rimraf: 3.0.2
@@ -4474,13 +4492,6 @@ packages:
'@sentry/utils': 7.54.0
dev: true
/@sentry/tracing@7.54.0:
resolution: {integrity: sha512-IyflAAqOiKuNvhWXrJsXN8yfl8DbHRpfgtCEPo5TBvdkpcyxUyfE8W2a2MQ8MXDzgfJT1IixXEwleO+qPIi3Gg==}
engines: {node: '>=8'}
dependencies:
'@sentry-internal/tracing': 7.54.0
dev: false
/@sentry/types@7.54.0:
resolution: {integrity: sha512-D+i9xogBeawvQi2r0NOrM7zYcUaPuijeME4O9eOTrDF20tj71hWtJLilK+KTGLYFtpGg1h+9bPaz7OHEIyVopg==}
engines: {node: '>=8'}
@@ -4894,8 +4905,8 @@ packages:
'@types/qs': 6.9.7
'@types/serve-static': 1.15.1
/@types/file-saver@2.0.5:
resolution: {integrity: sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==}
/@types/file-saver@2.0.2:
resolution: {integrity: sha512-xbqnZmGrCEqi/KUzOkeUSe77p7APvLuyellGaAoeww3CHJ1AbjQWjPSCFtKIzZn8L7LpEax4NXnC+gfa6nM7IA==}
dev: true
/@types/graceful-fs@4.1.6:
@@ -5069,6 +5080,12 @@ packages:
csstype: 3.1.2
dev: true
/@types/request-ip@0.0.41:
resolution: {integrity: sha512-Qzz0PM2nSZej4lsLzzNfADIORZhhxO7PED0fXpg4FjXiHuJ/lMyUg+YFF5q8x9HPZH3Gl6N+NOM8QZjItNgGKg==}
dependencies:
'@types/node': 17.0.45
dev: true
/@types/responselike@1.0.0:
resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==}
dependencies:
@@ -5172,7 +5189,7 @@ packages:
ignore: 5.2.4
natural-compare-lite: 1.4.0
regexpp: 3.2.0
semver: 7.5.1
semver: 7.5.4
tsutils: 3.21.0(typescript@5.2.2)
typescript: 5.2.2
transitivePeerDependencies:
@@ -5263,7 +5280,7 @@ packages:
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
semver: 7.5.1
semver: 7.5.4
tsutils: 3.21.0(typescript@5.2.2)
typescript: 5.2.2
transitivePeerDependencies:
@@ -5284,7 +5301,7 @@ packages:
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
semver: 7.5.1
semver: 7.5.4
tsutils: 3.21.0(typescript@5.2.2)
typescript: 5.2.2
transitivePeerDependencies:
@@ -5308,7 +5325,7 @@ packages:
eslint: 8.35.0
eslint-scope: 5.1.1
eslint-utils: 3.0.0(eslint@8.35.0)
semver: 7.5.1
semver: 7.5.4
transitivePeerDependencies:
- supports-color
- typescript
@@ -5331,7 +5348,7 @@ packages:
'@typescript-eslint/typescript-estree': 5.59.9(typescript@5.2.2)
eslint: 8.35.0
eslint-scope: 5.1.1
semver: 7.5.1
semver: 7.5.4
transitivePeerDependencies:
- supports-color
- typescript
@@ -6439,6 +6456,7 @@ packages:
/aproba@2.0.0:
resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==}
requiresBuild: true
dev: false
/are-we-there-yet@2.0.0:
@@ -6452,6 +6470,7 @@ packages:
/are-we-there-yet@3.0.1:
resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
requiresBuild: true
dependencies:
delegates: 1.0.0
readable-stream: 3.6.2
@@ -7019,12 +7038,11 @@ packages:
run-applescript: 5.0.0
dev: true
/busboy@0.2.14:
resolution: {integrity: sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==}
engines: {node: '>=0.8.0'}
/busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
dependencies:
dicer: 0.2.5
readable-stream: 1.1.14
streamsearch: 1.1.0
dev: false
/bytes@3.0.0:
@@ -7039,6 +7057,7 @@ packages:
/cacache@15.3.0:
resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==}
engines: {node: '>= 10'}
requiresBuild: true
dependencies:
'@npmcli/fs': 1.1.1
'@npmcli/move-file': 1.1.2
@@ -7326,6 +7345,7 @@ packages:
/color-support@1.1.3:
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
hasBin: true
requiresBuild: true
dev: false
/color@3.2.1:
@@ -7441,6 +7461,7 @@ packages:
/console-control-strings@1.1.0:
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
requiresBuild: true
dev: false
/constants-browserify@1.0.0:
@@ -7705,7 +7726,7 @@ packages:
postcss-modules-scope: 3.0.0(postcss@8.4.24)
postcss-modules-values: 4.0.0(postcss@8.4.24)
postcss-value-parser: 4.2.0
semver: 7.5.1
semver: 7.5.4
webpack: 5.85.1
dev: true
@@ -7952,6 +7973,7 @@ packages:
/delegates@1.0.0:
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
requiresBuild: true
dev: false
/depd@2.0.0:
@@ -7994,14 +8016,6 @@ packages:
resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
dev: true
/dicer@0.2.5:
resolution: {integrity: sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==}
engines: {node: '>=0.8.0'}
dependencies:
readable-stream: 1.1.14
streamsearch: 0.1.2
dev: false
/diff@4.0.2:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'}
@@ -8266,11 +8280,13 @@ packages:
/env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
requiresBuild: true
dev: false
optional: true
/err-code@2.0.3:
resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
requiresBuild: true
dev: false
optional: true
@@ -8911,8 +8927,8 @@ packages:
flat-cache: 3.0.4
dev: true
/file-saver@2.0.5:
resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
/file-saver@2.0.2:
resolution: {integrity: sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw==}
dev: true
/file-stream-rotator@0.6.1:
@@ -9039,7 +9055,7 @@ packages:
minimatch: 3.1.2
node-abort-controller: 3.1.1
schema-utils: 3.1.2
semver: 7.5.1
semver: 7.5.4
tapable: 2.2.1
typescript: 5.2.2
webpack: 5.85.1
@@ -9117,6 +9133,7 @@ packages:
/fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
requiresBuild: true
/fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
@@ -9168,6 +9185,7 @@ packages:
/gauge@4.0.4:
resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
requiresBuild: true
dependencies:
aproba: 2.0.0
color-support: 1.1.3
@@ -9432,6 +9450,7 @@ packages:
/has-unicode@2.0.1:
resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
requiresBuild: true
dev: false
/has@1.0.3:
@@ -9745,6 +9764,7 @@ packages:
/infer-owner@1.0.4:
resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==}
requiresBuild: true
dev: false
optional: true
@@ -9755,6 +9775,7 @@ packages:
/inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
requiresBuild: true
dependencies:
once: 1.4.0
wrappy: 1.0.2
@@ -9815,6 +9836,14 @@ packages:
loose-envify: 1.4.0
dev: true
/ip2region@2.3.0(@types/node@17.0.45):
resolution: {integrity: sha512-zV5Xsadzrx9Ej6heoyhbXMsfGWWQ3C6bAIYStrHhw9kzLpGpVNlnAyRBxxPgxA1GNqr1Ti7oUxcWsMWNN3jZBg==}
peerDependencies:
'@types/node': '*'
dependencies:
'@types/node': 17.0.45
dev: false
/ip@1.1.8:
resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==}
dev: true
@@ -9996,6 +10025,7 @@ packages:
/is-lambda@1.0.1:
resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==}
requiresBuild: true
dev: false
optional: true
@@ -10792,6 +10822,7 @@ packages:
/make-fetch-happen@9.1.0:
resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==}
engines: {node: '>= 10'}
requiresBuild: true
dependencies:
agentkeepalive: 4.3.0
cacache: 15.3.0
@@ -11006,6 +11037,7 @@ packages:
/minipass-collect@1.0.2:
resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==}
engines: {node: '>= 8'}
requiresBuild: true
dependencies:
minipass: 3.3.6
dev: false
@@ -11014,6 +11046,7 @@ packages:
/minipass-fetch@1.4.1:
resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==}
engines: {node: '>=8'}
requiresBuild: true
dependencies:
minipass: 3.3.6
minipass-sized: 1.0.3
@@ -11026,6 +11059,7 @@ packages:
/minipass-flush@1.0.5:
resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==}
engines: {node: '>= 8'}
requiresBuild: true
dependencies:
minipass: 3.3.6
dev: false
@@ -11034,6 +11068,7 @@ packages:
/minipass-pipeline@1.2.4:
resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==}
engines: {node: '>=8'}
requiresBuild: true
dependencies:
minipass: 3.3.6
dev: false
@@ -11042,6 +11077,7 @@ packages:
/minipass-sized@1.0.3:
resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==}
engines: {node: '>=8'}
requiresBuild: true
dependencies:
minipass: 3.3.6
dev: false
@@ -11110,17 +11146,15 @@ packages:
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
/multer@1.4.4:
resolution: {integrity: sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==}
engines: {node: '>= 0.10.0'}
deprecated: Multer 1.x is affected by CVE-2022-24434. This is fixed in v1.4.4-lts.1 which drops support for versions of Node.js before 6. Please upgrade to at least Node.js 6 and version 1.4.4-lts.1 of Multer. If you need support for older versions of Node.js, we are open to accepting patches that would fix the CVE on the main 1.x release line, whilst maintaining compatibility with Node.js 0.10.
/multer@1.4.5-lts.1:
resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==}
engines: {node: '>= 6.0.0'}
dependencies:
append-field: 1.0.0
busboy: 0.2.14
busboy: 1.6.0
concat-stream: 1.6.2
mkdirp: 0.5.6
object-assign: 4.1.1
on-finished: 2.4.1
type-is: 1.6.18
xtend: 4.0.2
dev: false
@@ -11209,8 +11243,8 @@ packages:
engines: {node: '>=10.5.0'}
dev: true
/node-fetch@2.6.11:
resolution: {integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==}
/node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
@@ -11243,7 +11277,7 @@ packages:
nopt: 5.0.0
npmlog: 6.0.2
rimraf: 3.0.2
semver: 7.5.1
semver: 7.5.4
tar: 6.1.15
which: 2.0.2
transitivePeerDependencies:
@@ -11302,9 +11336,9 @@ packages:
engines: {node: '>=6.0.0'}
dev: false
/nodemon@2.0.22:
resolution: {integrity: sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==}
engines: {node: '>=8.10.0'}
/nodemon@3.0.1:
resolution: {integrity: sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==}
engines: {node: '>=10'}
hasBin: true
dependencies:
chokidar: 3.5.3
@@ -11312,8 +11346,8 @@ packages:
ignore-by-default: 1.0.1
minimatch: 3.1.2
pstree.remy: 1.1.8
semver: 5.7.1
simple-update-notifier: 1.1.0
semver: 7.5.4
simple-update-notifier: 2.0.0
supports-color: 5.5.0
touch: 3.1.0
undefsafe: 2.0.5
@@ -11330,6 +11364,7 @@ packages:
resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
engines: {node: '>=6'}
hasBin: true
requiresBuild: true
dependencies:
abbrev: 1.1.1
dev: false
@@ -11349,7 +11384,7 @@ packages:
dependencies:
hosted-git-info: 4.1.0
is-core-module: 2.12.1
semver: 7.5.1
semver: 7.5.4
validate-npm-package-license: 3.0.4
dev: true
@@ -11413,6 +11448,7 @@ packages:
/npmlog@6.0.2:
resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
requiresBuild: true
dependencies:
are-we-there-yet: 3.0.1
console-control-strings: 1.1.0
@@ -11766,6 +11802,7 @@ packages:
/path-is-absolute@1.0.1:
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'}
requiresBuild: true
/path-is-inside@1.0.2:
resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==}
@@ -12496,6 +12533,11 @@ packages:
hasBin: true
dev: true
/pretty-bytes@6.1.1:
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
engines: {node: ^14.13.1 || >=16.0.0}
dev: true
/pretty-error@4.0.0:
resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==}
dependencies:
@@ -12517,6 +12559,7 @@ packages:
/promise-inflight@1.0.1:
resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
requiresBuild: true
peerDependencies:
bluebird: '*'
peerDependenciesMeta:
@@ -12528,6 +12571,7 @@ packages:
/promise-retry@2.0.1:
resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
engines: {node: '>=10'}
requiresBuild: true
dependencies:
err-code: 2.0.3
retry: 0.12.0
@@ -12615,6 +12659,7 @@ packages:
/prr@1.0.1:
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
requiresBuild: true
dev: true
optional: true
@@ -12622,6 +12667,10 @@ packages:
resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==}
dev: true
/psl@1.9.0:
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
dev: false
/pstree.remy@1.1.8:
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
@@ -12652,7 +12701,6 @@ packages:
/punycode@2.3.0:
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
engines: {node: '>=6'}
dev: true
/qiankun@2.10.8:
resolution: {integrity: sha512-5w6QeM1INFavJdIWfSoDIVv2InV8MEojauvY5tY9Biu8hhE5Rml/HM2t/Y60kZcuOpgy/HGX+tcc6NkQuzDoCg==}
@@ -13747,6 +13795,21 @@ packages:
shallowequal: 1.1.0
dev: true
/react-hotkeys-hook@4.4.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-sClBMBioFEgFGYLTWWRKvhxcCx1DRznd+wkFHwQZspnRBkHTgruKIHptlK/U/2DPX8BhHoRGzpMVWUXMmdZlmw==}
peerDependencies:
react: '>=16.8.1 || 18'
react-dom: '>=16.8.1 || 18'
peerDependenciesMeta:
react:
optional: true
react-dom:
optional: true
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: true
/react-intl-universal@2.6.21(react@18.2.0):
resolution: {integrity: sha512-JJqZMzdB6jd4f0NfGRc9XM/pZElxtrDCWC9pE2yIesDO70SL+jQgaFAucQmstPwYqCcjvLpCgOrjP7cIo4kJnw==}
requiresBuild: true
@@ -14003,6 +14066,7 @@ packages:
inherits: 2.0.4
isarray: 0.0.1
string_decoder: 0.10.31
dev: true
/readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
@@ -14145,6 +14209,10 @@ packages:
strip-ansi: 6.0.1
dev: true
/request-ip@3.3.0:
resolution: {integrity: sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA==}
dev: false
/require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
@@ -14218,6 +14286,7 @@ packages:
/retry@0.12.0:
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
engines: {node: '>= 4'}
requiresBuild: true
dev: false
optional: true
@@ -14309,6 +14378,7 @@ packages:
/sax@1.2.4:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
requiresBuild: true
dev: true
optional: true
@@ -14366,17 +14436,20 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
/semver@7.0.0:
resolution: {integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==}
hasBin: true
dev: true
/semver@7.5.1:
resolution: {integrity: sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==}
engines: {node: '>=10'}
hasBin: true
dependencies:
lru-cache: 6.0.0
dev: false
/semver@7.5.4:
resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
engines: {node: '>=10'}
hasBin: true
dependencies:
lru-cache: 6.0.0
/send@0.18.0:
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
@@ -14491,6 +14564,7 @@ packages:
/set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
requiresBuild: true
dev: false
/setimmediate@1.0.5:
@@ -14558,11 +14632,11 @@ packages:
is-arrayish: 0.3.2
dev: false
/simple-update-notifier@1.1.0:
resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==}
engines: {node: '>=8.10.0'}
/simple-update-notifier@2.0.0:
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
engines: {node: '>=10'}
dependencies:
semver: 7.0.0
semver: 7.5.4
dev: true
/single-spa@5.9.4:
@@ -14649,6 +14723,7 @@ packages:
/socks-proxy-agent@6.2.1:
resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==}
engines: {node: '>= 10'}
requiresBuild: true
dependencies:
agent-base: 6.0.2
debug: 4.3.4
@@ -14792,6 +14867,7 @@ packages:
/ssri@8.0.1:
resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==}
engines: {node: '>= 8'}
requiresBuild: true
dependencies:
minipass: 3.3.6
dev: false
@@ -14851,9 +14927,9 @@ packages:
resolution: {integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==}
dev: true
/streamsearch@0.1.2:
resolution: {integrity: sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==}
engines: {node: '>=0.8.0'}
/streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
dev: false
/strict-uri-encode@2.0.0:
@@ -14927,6 +15003,7 @@ packages:
/string_decoder@0.10.31:
resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==}
dev: true
/string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
@@ -15389,6 +15466,15 @@ packages:
nopt: 1.0.10
dev: true
/tough-cookie@4.0.0:
resolution: {integrity: sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==}
engines: {node: '>=6'}
dependencies:
psl: 1.9.0
punycode: 2.3.0
universalify: 0.1.2
dev: false
/tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
dev: false
@@ -15703,6 +15789,7 @@ packages:
/unique-filename@1.1.1:
resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==}
requiresBuild: true
dependencies:
unique-slug: 2.0.2
dev: false
@@ -15710,6 +15797,7 @@ packages:
/unique-slug@2.0.2:
resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==}
requiresBuild: true
dependencies:
imurmurhash: 0.1.4
dev: false
@@ -15718,7 +15806,6 @@ packages:
/universalify@0.1.2:
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
engines: {node: '>= 4.0.0'}
dev: true
/universalify@2.0.0:
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
@@ -16162,6 +16249,7 @@ packages:
/wide-align@1.1.5:
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
requiresBuild: true
dependencies:
string-width: 4.2.3
dev: false
@@ -16242,6 +16330,7 @@ packages:
/wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
requiresBuild: true
/write-file-atomic@4.0.2:
resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==}
@@ -16335,7 +16424,7 @@ packages:
node-gyp:
optional: true
dependencies:
'@mapbox/node-pre-gyp': 1.0.10
'@mapbox/node-pre-gyp': 1.0.11
node-addon-api: 4.3.0
tar: 6.1.15
optionalDependencies:
@@ -16345,3 +16434,7 @@ packages:
- encoding
- supports-color
dev: false
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+10 -20
View File
@@ -21,20 +21,19 @@ DiskWarn=90
## 设置定时任务执行的超时时间,例如1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
CommandTimeoutTime=""
## 在运行 task 命令时,随机延迟启动任务的最大延迟时间
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
RandomDelay="300"
## 在运行 task 命令时,随机延迟启动任务的最大延迟时间,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
RandomDelay=""
## 需要随机延迟运行任务的文件后缀,直接写后缀名即可,多个后缀用空格分开,例如: js py ts
## 默认仅给javascript任务加随机延迟,其它任务按定时规则准点运行。全部任务随机延迟赋值为空
RandomDelayFileExtensions="js"
RandomDelayFileExtensions=""
## 每小时的第几分钟准点运行任务,当在这些时间运行任务时将忽略 RandomDelay 配置,不会被随机延迟
## 默认是第0分钟和第30分钟,例如21:00或21:30分的任务将会准点运行。不需要准点运行赋值为空
RandomDelayIgnoredMinutes="0 30"
RandomDelayIgnoredMinutes=""
## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true
EnableExtraShell="true"
## 如果你自己会写shell脚本,并且希望在每次容器启动时,额外运行你的 shell 脚本,请赋值为 "true"
EnableExtraShell=""
## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot
AutoStartBot=""
@@ -42,12 +41,6 @@ AutoStartBot=""
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
BotRepoUrl=""
## 安装python依赖时指定pip源
PipMirror="https://pypi.doubanio.com/simple/"
## 安装node依赖时指定npm源
NpmMirror="https://registry.npmmirror.com"
## 通知环境变量
## 1. Server酱
## https://sct.ftqq.com
@@ -156,11 +149,8 @@ export AIBOTK_TYPE=""
export AIBOTK_NAME=""
## 14. SMTP
## 暂时只支持在 Python 中调用 notify.py 以发送 SMTP 邮件通知
## smtp_server 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
export SMTP_SERVER=""
## smtp_ssl 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
export SMTP_SSL="false"
## 邮箱服务名称,比如126、163、Gmail、QQ等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json
export SMTP_SERVICE=""
## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己
export SMTP_EMAIL=""
## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
@@ -178,8 +168,8 @@ export PUSHME_KEY=""
## CHRONOCAT_TOKEN 填写在CHRONOCAT文件生成的访问密钥
## CHRONOCAT_QQ 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx
## CHRONOCAT相关API https://chronocat.vercel.app/install/docker/official/
export CHRONOCAT_URL=""
export CHRONOCAT_QQ="" #
export CHRONOCAT_URL=""
export CHRONOCAT_QQ=""
export CHRONOCAT_TOKEN=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+152 -158
View File
@@ -13,81 +13,81 @@
const querystring = require('querystring');
const got = require('got');
const $ = new Env();
const timeout = 15000; //超时时间(单位毫秒)
const timeout = 15000; // 超时时间(单位毫秒)
// =======================================gotify通知设置区域==============================================
//gotify_url 填写gotify地址,如https://push.example.de:8080
//gotify_token 填写gotify的消息应用token
//gotify_priority 填写推送消息优先级,默认为0
// gotify_url 填写gotify地址,如https://push.example.de:8080
// gotify_token 填写gotify的消息应用token
// gotify_priority 填写推送消息优先级,默认为0
let GOTIFY_URL = '';
let GOTIFY_TOKEN = '';
let GOTIFY_PRIORITY = 0;
// =======================================go-cqhttp通知设置区域===========================================
//gobot_url 填写请求地址http://127.0.0.1/send_private_msg
//gobot_token 填写在go-cqhttp文件设置的访问密钥
//gobot_qq 填写推送到个人QQ或者QQ群号
//go-cqhttp相关API https://docs.go-cqhttp.org/api
// gobot_url 填写请求地址http://127.0.0.1/send_private_msg
// gobot_token 填写在go-cqhttp文件设置的访问密钥
// gobot_qq 填写推送到个人QQ或者QQ群号
// go-cqhttp相关API https://docs.go-cqhttp.org/api
let GOBOT_URL = ''; // 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
let GOBOT_TOKEN = ''; //访问密钥
let GOBOT_TOKEN = ''; // 访问密钥
let GOBOT_QQ = ''; // 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
// =======================================微信server酱通知设置区域===========================================
//此处填你申请的SCKEY.
//(环境变量名 PUSH_KEY)
// 此处填你申请的SCKEY.
// (环境变量名 PUSH_KEY)
let SCKEY = '';
// =======================================PushDeer通知设置区域===========================================
//此处填你申请的PushDeer KEY.
//(环境变量名 DEER_KEY)
// 此处填你申请的PushDeer KEY.
// (环境变量名 DEER_KEY)
let PUSHDEER_KEY = '';
let PUSHDEER_URL = '';
// =======================================Synology Chat通知设置区域===========================================
//此处填你申请的CHAT_URL与CHAT_TOKEN
//(环境变量名 CHAT_URL CHAT_TOKEN)
// 此处填你申请的CHAT_URL与CHAT_TOKEN
// (环境变量名 CHAT_URL CHAT_TOKEN)
let CHAT_URL = '';
let CHAT_TOKEN = '';
// =======================================Bark App通知设置区域===========================================
//此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
// 此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
let BARK_PUSH = '';
//BARK app推送图标,自定义推送图标(需iOS15或以上)
// BARK app推送图标,自定义推送图标(需iOS15或以上)
let BARK_ICON = 'https://qn.whyour.cn/logo.png';
//BARK app推送铃声,铃声列表去APP查看复制填写
// BARK app推送铃声,铃声列表去APP查看复制填写
let BARK_SOUND = '';
//BARK app推送消息的分组, 默认为"QingLong"
// BARK app推送消息的分组, 默认为"QingLong"
let BARK_GROUP = 'QingLong';
//BARK app推送消息的时效性, 默认为"active"
// BARK app推送消息的时效性, 默认为"active"
let BARK_LEVEL = 'active';
//BARK app推送消息的跳转URL
// BARK app推送消息的跳转URL
let BARK_URL = '';
// =======================================telegram机器人通知设置区域===========================================
//此处填你telegram bot 的Tokentelegram机器人通知推送必填项.例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw
//(环境变量名 TG_BOT_TOKEN)
// 此处填你telegram bot 的Tokentelegram机器人通知推送必填项.例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw
// (环境变量名 TG_BOT_TOKEN)
let TG_BOT_TOKEN = '';
//此处填你接收通知消息的telegram用户的id,telegram机器人通知推送必填项.例如:129xxx206
//(环境变量名 TG_USER_ID)
// 此处填你接收通知消息的telegram用户的id,telegram机器人通知推送必填项.例如:129xxx206
// (环境变量名 TG_USER_ID)
let TG_USER_ID = '';
//tg推送HTTP代理设置(不懂可忽略,telegram机器人通知推送功能中非必填)
let TG_PROXY_HOST = ''; //例如:127.0.0.1(环境变量名:TG_PROXY_HOST)
let TG_PROXY_PORT = ''; //例如:1080(环境变量名:TG_PROXY_PORT)
let TG_PROXY_AUTH = ''; //tg代理配置认证参数
//Telegram api自建的反向代理地址(不懂可忽略,telegram机器人通知推送功能中非必填),默认tg官方api(环境变量名:TG_API_HOST)
let TG_API_HOST = 'api.telegram.org';
// tg推送HTTP代理设置(不懂可忽略,telegram机器人通知推送功能中非必填)
let TG_PROXY_HOST = ''; // 例如:127.0.0.1(环境变量名:TG_PROXY_HOST)
let TG_PROXY_PORT = ''; // 例如:1080(环境变量名:TG_PROXY_PORT)
let TG_PROXY_AUTH = ''; // tg代理配置认证参数
// Telegram api自建的反向代理地址(不懂可忽略,telegram机器人通知推送功能中非必填),默认tg官方api(环境变量名:TG_API_HOST)
let TG_API_HOST = 'https://api.telegram.org';
// =======================================钉钉机器人通知设置区域===========================================
//此处填你钉钉 bot 的webhook,例如:5a544165465465645d0f31dca676e7bd07415asdasd
//(环境变量名 DD_BOT_TOKEN)
// 此处填你钉钉 bot 的webhook,例如:5a544165465465645d0f31dca676e7bd07415asdasd
// (环境变量名 DD_BOT_TOKEN)
let DD_BOT_TOKEN = '';
//密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串
// 密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串
let DD_BOT_SECRET = '';
// =======================================企业微信基础设置===========================================
// 企业微信反向代理地址
//(环境变量名 QYWX_ORIGIN)
// (环境变量名 QYWX_ORIGIN)
let QYWX_ORIGIN = '';
// =======================================企业微信机器人通知设置区域===========================================
//此处填你企业微信机器人的 webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa
//(环境变量名 QYWX_KEY)
// 此处填你企业微信机器人的 webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa
// (环境变量名 QYWX_KEY)
let QYWX_KEY = '';
// =======================================企业微信应用消息通知设置区域===========================================
@@ -103,52 +103,43 @@ let QYWX_KEY = '';
let QYWX_AM = '';
// =======================================iGot聚合推送通知设置区域===========================================
//此处填您iGot的信息(推送key,例如:https://push.hellyw.com/XXXXXXXX)
// 此处填您iGot的信息(推送key,例如:https://push.hellyw.com/XXXXXXXX)
let IGOT_PUSH_KEY = '';
// =======================================push+设置区域=======================================
//官方文档:http://www.pushplus.plus/
//PUSH_PLUS_TOKEN:微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送
//PUSH_PLUS_USER: 一对多推送的“群组编码”(一对多推送下面->您的群组(如无则新建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)
// 官方文档:http://www.pushplus.plus/
// PUSH_PLUS_TOKEN:微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送
// PUSH_PLUS_USER: 一对多推送的“群组编码”(一对多推送下面->您的群组(如无则新建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)
let PUSH_PLUS_TOKEN = '';
let PUSH_PLUS_USER = '';
// =======================================Cool Push设置区域=======================================
//官方文档:https://cp.xuthus.cc/docs
//QQ_SKEY: Cool Push登录授权后推送消息的调用代码Skey
//QQ_MODE: 推送模式详情请登录获取QQ_SKEY后见https://cp.xuthus.cc/feat
let QQ_SKEY = '';
let QQ_MODE = '';
// =======================================智能微秘书设置区域=======================================
//官方文档:http://wechat.aibotk.com/docs/about
//AIBOTK_KEY 填写智能微秘书个人中心的apikey
//AIBOTK_TYPE:填写发送的目标 room 或 contact, 填其他的不生效
//AIBOTK_NAME: 填写群名或用户昵称,和上面的type类型要对应
// 官方文档:http://wechat.aibotk.com/docs/about
// AIBOTK_KEY 填写智能微秘书个人中心的apikey
// AIBOTK_TYPE:填写发送的目标 room 或 contact, 填其他的不生效
// AIBOTK_NAME: 填写群名或用户昵称,和上面的type类型要对应
let AIBOTK_KEY = '';
let AIBOTK_TYPE = '';
let AIBOTK_NAME = '';
// =======================================飞书机器人设置区域=======================================
//官方文档:https://www.feishu.cn/hc/zh-CN/articles/360024984973
//FSKEY 飞书机器人的 FSKEY
// 官方文档:https://www.feishu.cn/hc/zh-CN/articles/360024984973
// FSKEY 飞书机器人的 FSKEY
let FSKEY = '';
// =======================================SMTP 邮件设置区域=======================================
// SMTP_SERVER: 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
// SMTP_SSL: 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
// SMTP_SERVICE: 邮箱服务名称,比如126、163、Gmail、QQ等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json
// SMTP_EMAIL: 填写 SMTP 收发件邮箱,通知将会由自己发给自己
// SMTP_PASSWORD: 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
// SMTP_NAME: 填写 SMTP 收发件人姓名,可随意填写
let SMTP_SERVER = '';
let SMTP_SSL = 'false';
let SMTP_SERVICE = '';
let SMTP_EMAIL = '';
let SMTP_PASSWORD = '';
let SMTP_NAME = '';
// =======================================PushMe通知设置区域===========================================
//官方文档:https://push.i-i.me/
//此处填你的PushMe KEY.
// 官方文档:https://push.i-i.me/
// 此处填你的PushMe KEY.
let PUSHME_KEY = '';
// =======================================CHRONOCAT通知设置区域===========================================
@@ -157,7 +148,7 @@ let PUSHME_KEY = '';
// CHRONOCAT_QQ 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群
// CHRONOCAT相关API https://chronocat.vercel.app/install/docker/official/
let CHRONOCAT_URL = ''; // CHRONOCAT Red协议连接地址
let CHRONOCAT_TOKEN = ''; //CHRONOCAT 生成的访问密钥
let CHRONOCAT_TOKEN = ''; // CHRONOCAT 生成的访问密钥
let CHRONOCAT_QQ = ''; // 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx
// =======================================自定义通知设置区域=======================================
@@ -168,7 +159,7 @@ let WEBHOOK_HEADERS = '';
let WEBHOOK_METHOD = '';
let WEBHOOK_CONTENT_TYPE = '';
//==========================云端环境变量的判断与接收=========================
// ==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL;
}
@@ -219,7 +210,7 @@ if (process.env.BARK_PUSH) {
process.env.BARK_PUSH.indexOf('https') > -1 ||
process.env.BARK_PUSH.indexOf('http') > -1
) {
//兼容BARK自建用户
// 兼容BARK自建用户
BARK_PUSH = process.env.BARK_PUSH;
} else {
BARK_PUSH = `https://api.day.app/${process.env.BARK_PUSH}`;
@@ -245,7 +236,7 @@ if (process.env.BARK_PUSH) {
BARK_PUSH.indexOf('https') === -1 &&
BARK_PUSH.indexOf('http') === -1
) {
//兼容BARK本地用户只填写设备码的情况
// 兼容BARK本地用户只填写设备码的情况
BARK_PUSH = `https://api.day.app/${BARK_PUSH}`;
}
}
@@ -306,11 +297,8 @@ if (process.env.FSKEY) {
FSKEY = process.env.FSKEY;
}
if (process.env.SMTP_SERVER) {
SMTP_SERVER = process.env.SMTP_SERVER;
}
if (process.env.SMTP_SSL) {
SMTP_SSL = process.env.SMTP_SSL;
if (process.env.SMTP_SERVICE) {
SMTP_SERVICE = process.env.SMTP_SERVICE;
}
if (process.env.SMTP_EMAIL) {
SMTP_EMAIL = process.env.SMTP_EMAIL;
@@ -350,7 +338,7 @@ if (process.env.WEBHOOK_METHOD) {
if (process.env.WEBHOOK_CONTENT_TYPE) {
WEBHOOK_CONTENT_TYPE = process.env.WEBHOOK_CONTENT_TYPE;
}
//==========================云端环境变量的判断与接收=========================
// ==========================云端环境变量的判断与接收=========================
/**
* sendNotify 推送通知功能
@@ -366,8 +354,8 @@ async function sendNotify(
params = {},
author = '\n\n本通知 Byhttps://github.com/whyour/qinglong',
) {
//提供6种通知
desp += author; //增加作者信息,防止被贩卖等
// 提供6种通知
desp += author; // 增加作者信息,防止被贩卖等
// 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
let skipTitle = process.env.SKIP_PUSH_TITLE;
@@ -379,28 +367,28 @@ async function sendNotify(
}
await Promise.all([
serverNotify(text, desp), //微信server酱
pushPlusNotify(text, desp), //pushplus(推送加)
serverNotify(text, desp), // 微信server酱
pushPlusNotify(text, desp), // pushplus(推送加)
]);
//由于上述两种微信通知需点击进去才能查看到详情,故text(标题内容)携带了账号序号以及昵称信息,方便不点击也可知道是哪个京东哪个活动
// 由于上述两种微信通知需点击进去才能查看到详情,故text(标题内容)携带了账号序号以及昵称信息,方便不点击也可知道是哪个京东哪个活动
text = text.match(/.*?(?=\s?-)/g) ? text.match(/.*?(?=\s?-)/g)[0] : text;
await Promise.all([
BarkNotify(text, desp, params), //iOS Bark APP
tgBotNotify(text, desp), //telegram 机器人
ddBotNotify(text, desp), //钉钉机器人
qywxBotNotify(text, desp), //企业微信机器人
qywxamNotify(text, desp), //企业微信应用消息推送
iGotNotify(text, desp, params), //iGot
gobotNotify(text, desp), //go-cqhttp
gotifyNotify(text, desp), //gotify
ChatNotify(text, desp), //synolog chat
PushDeerNotify(text, desp), //PushDeer
aibotkNotify(text, desp), //智能微秘书
fsBotNotify(text, desp), //飞书机器人
smtpNotify(text, desp), //SMTP 邮件
PushMeNotify(text, desp, params), //PushMe
ChronocatNotify(text, desp), // Chronocat
webhookNotify(text, desp), //自定义通知
BarkNotify(text, desp, params), // iOS Bark APP
tgBotNotify(text, desp), // telegram 机器人
ddBotNotify(text, desp), // 钉钉机器人
qywxBotNotify(text, desp), // 企业微信机器人
qywxamNotify(text, desp), // 企业微信应用消息推送
iGotNotify(text, desp, params), // iGot
gobotNotify(text, desp), // go-cqhttp
gotifyNotify(text, desp), // gotify
ChatNotify(text, desp), // synolog chat
PushDeerNotify(text, desp), // PushDeer
aibotkNotify(text, desp), // 智能微秘书
fsBotNotify(text, desp), // 飞书机器人
smtpNotify(text, desp), // SMTP 邮件
pushMeNotify(text, desp, params), // PushMe
chronocatNotify(text, desp), // Chronocat
webhookNotify(text, desp), // 自定义通知
]);
}
@@ -482,7 +470,7 @@ function gobotNotify(text, desp) {
function serverNotify(text, desp) {
return new Promise((resolve) => {
if (SCKEY) {
//微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换
// 微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换
desp = desp.replace(/[\n\r]/g, '\n\n');
const options = {
url: SCKEY.includes('SCT')
@@ -501,7 +489,7 @@ function serverNotify(text, desp) {
console.log(err);
} else {
data = JSON.parse(data);
//server酱和Server酱·Turbo版的返回json格式不太一样
// server酱和Server酱·Turbo版的返回json格式不太一样
if (data.errno === 0 || data.data.errno === 0) {
console.log('server酱发送通知消息成功🎉\n');
} else if (data.errno === 1024) {
@@ -645,7 +633,7 @@ function tgBotNotify(text, desp) {
return new Promise((resolve) => {
if (TG_BOT_TOKEN && TG_USER_ID) {
const options = {
url: `https://${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`,
url: `${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`,
json: {
chat_id: `${TG_USER_ID}`,
text: `${text}\n\n${desp}`,
@@ -657,15 +645,19 @@ function tgBotNotify(text, desp) {
timeout,
};
if (TG_PROXY_HOST && TG_PROXY_PORT) {
const tunnel = require('tunnel');
const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent');
const options = {
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 256,
maxFreeSockets: 256,
proxy: `http://${TG_PROXY_AUTH}${TG_PROXY_HOST}:${TG_PROXY_PORT}`,
};
const httpAgent = new HttpProxyAgent(options);
const httpsAgent = new HttpsProxyAgent(options);
const agent = {
https: tunnel.httpsOverHttp({
proxy: {
host: TG_PROXY_HOST,
port: TG_PROXY_PORT * 1,
proxyAuth: TG_PROXY_AUTH,
},
}),
http: httpAgent,
https: httpsAgent,
};
Object.assign(options, { agent });
}
@@ -824,7 +816,18 @@ function ChangeUserId(desp) {
}
}
function qywxamNotify(text, desp) {
async function qywxamNotify(text, desp) {
const MAX_LENGTH = 900;
if (desp.length > MAX_LENGTH) {
let d = desp.substr(0, MAX_LENGTH) + "\n==More==";
await do_qywxamNotify(text, d);
await qywxamNotify(text, desp.substr(MAX_LENGTH));
} else {
return await do_qywxamNotify(text,desp);
}
}
function do_qywxamNotify(text, desp) {
return new Promise((resolve) => {
if (QYWX_AM) {
const QYWX_AM_AY = QYWX_AM.split(',');
@@ -885,7 +888,7 @@ function qywxamNotify(text, desp) {
};
}
if (!QYWX_AM_AY[4]) {
//如不提供第四个参数,则默认进行文本消息类型推送
// 如不提供第四个参数,则默认进行文本消息类型推送
options = {
msgtype: 'text',
text: {
@@ -1137,31 +1140,31 @@ function fsBotNotify(text, desp) {
}
async function smtpNotify(text, desp) {
if (![SMTP_SERVER, SMTP_EMAIL, SMTP_PASSWORD].every(Boolean)) {
if (![SMTP_EMAIL, SMTP_PASSWORD].every(Boolean) || !SMTP_SERVICE) {
return;
}
try {
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport(
`${SMTP_SSL === 'true' ? 'smtps:' : 'smtp:'}//${SMTP_SERVER}`,
{
auth: {
user: SMTP_EMAIL,
pass: SMTP_PASSWORD,
},
const transporter = nodemailer.createTransport({
service: SMTP_SERVICE,
auth: {
user: SMTP_EMAIL,
pass: SMTP_PASSWORD,
},
);
});
const addr = SMTP_NAME ? `"${SMTP_NAME}" <${SMTP_EMAIL}>` : SMTP_EMAIL;
const info = await transporter.sendMail({
from: addr,
to: addr,
subject: text,
text: desp,
html: `${desp.replace(/\n/g, '<br/>')}`,
});
if (!!info.messageId) {
transporter.close();
if (info.messageId) {
console.log('SMTP发送通知消息成功🎉\n');
return true;
}
@@ -1172,17 +1175,7 @@ async function smtpNotify(text, desp) {
}
}
function smtpNotify(text, desp) {
return new Promise((resolve) => {
if (SMTP_SERVER && SMTP_SSL && SMTP_EMAIL && SMTP_PASSWORD && SMTP_NAME) {
// todo: Node.js并没有内置的 smtp 实现,需要调用外部库,因为不清楚这个文件的模块依赖情况,所以留给有缘人实现
} else {
resolve();
}
});
}
function PushMeNotify(text, desp, params = {}) {
function pushMeNotify(text, desp, params = {}) {
return new Promise((resolve) => {
if (PUSHME_KEY) {
const options = {
@@ -1217,16 +1210,13 @@ function PushMeNotify(text, desp, params = {}) {
});
}
function ChronocatNotify(title, desp) {
function chronocatNotify(title, desp) {
return new Promise((resolve) => {
if (!CHRONOCAT_TOKEN || !CHRONOCAT_QQ || !CHRONOCAT_URL) {
console.log(
'CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\n取消推送',
);
resolve();
return;
}
console.log('CHRONOCAT 服务启动');
const user_ids = CHRONOCAT_QQ.match(/user_id=(\d+)/g)?.map(
(match) => match.split('=')[1],
);
@@ -1320,11 +1310,9 @@ function webhookNotify(text, desp) {
got(formatUrl, options).then((resp) => {
try {
if (resp.statusCode !== 200) {
console.log('自定义发送通知消息失败!!\n');
console.log(resp.body);
console.log(`自定义发送通知消息失败!!\n${resp.body}`);
} else {
console.log('自定义发送通知消息成功🎉。\n');
console.log(resp.body);
console.log(`自定义发送通知消息成功🎉。\n${resp.body}`);
}
} catch (e) {
$.logErr(e, resp);
@@ -1338,6 +1326,31 @@ function webhookNotify(text, desp) {
});
}
function parseString(input) {
const regex = /(\w+):\s*((?:(?!\n\w+:).)*)/g;
const matches = {};
let match;
while ((match = regex.exec(input)) !== null) {
const [, key, value] = match;
const _key = key.trim();
if (!_key || matches[_key]) {
continue;
}
const _value = value.trim();
try {
const jsonValue = JSON.parse(_value);
matches[_key] = jsonValue;
} catch (error) {
matches[_key] = _value;
}
}
return matches;
}
function parseHeaders(headers) {
if (!headers) return {};
@@ -1363,30 +1376,11 @@ function parseHeaders(headers) {
}
function parseBody(body, contentType) {
if (!body) return '';
if (contentType === 'text/plain' || !body) {
return body;
}
const parsed = {};
let key;
let val;
let i;
body &&
body.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key || parsed[key]) {
return;
}
try {
const jsonValue = JSON.parse(val);
parsed[key] = jsonValue;
} catch (error) {
parsed[key] = val;
}
});
const parsed = parseString(body);
switch (contentType) {
case 'multipart/form-data':
+81 -83
View File
@@ -79,7 +79,7 @@ push_config = {
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
'QYWX_ORIGIN': '', # 企业微信代理地址
'QYWX_AM': '', # 企业微信应用
'QYWX_KEY': '', # 企业微信机器人
@@ -102,11 +102,11 @@ push_config = {
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
'PUSHME_KEY': '', # PushMe 酱的 PUSHME_KEY
'CHRONOCAT_QQ': '', # qq号
'CHRONOCAT_TOKEN': '', # CHRONOCAT 的token
'CHRONOCAT_URL': '', # CHRONOCAT的url地址
'WEBHOOK_URL': '', # 自定义通知 请求地址
'WEBHOOK_BODY': '', # 自定义通知 请求体
'WEBHOOK_HEADERS': '', # 自定义通知 请求头
@@ -534,7 +534,7 @@ def telegram_bot(title: str, content: str) -> None:
print("tg 服务启动")
if push_config.get("TG_API_HOST"):
url = f"https://{push_config.get('TG_API_HOST')}/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
url = f"{push_config.get('TG_API_HOST')}/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
else:
url = (
f"https://api.telegram.org/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
@@ -578,7 +578,9 @@ def aibotk(title: str, content: str) -> None:
or not push_config.get("AIBOTK_TYPE")
or not push_config.get("AIBOTK_NAME")
):
print("智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送")
print(
"智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送"
)
return
print("智能微秘书 服务启动")
@@ -748,29 +750,25 @@ def parse_headers(headers):
return parsed
def parse_body(body, content_type):
if not body:
return ""
parsed = {}
lines = body.split("\n")
for line in lines:
i = line.find(":")
if i == -1:
continue
key = line[:i].strip().lower()
val = line[i + 1 :].strip()
if not key or key in parsed:
continue
def parse_string(input_string):
matches = {}
pattern = r'(\w+):\s*((?:(?!\n\w+:).)*)'
regex = re.compile(pattern)
for match in regex.finditer(input_string):
key, value = match.group(1).strip(), match.group(2).strip()
try:
json_value = json.loads(val)
parsed[key] = json_value
json_value = json.loads(value)
matches[key] = json_value
except:
parsed[key] = val
matches[key] = value
return matches
def parse_body(body, content_type):
if not body or content_type == "text/plain":
return body
parsed = parse_string(input_string)
if content_type == "application/x-www-form-urlencoded":
data = urlencode(parsed, doseq=True)
@@ -841,60 +839,61 @@ def one() -> str:
return res["hitokoto"] + " ----" + res["from"]
if push_config.get("BARK_PUSH"):
notify_function.append(bark)
if push_config.get("CONSOLE"):
notify_function.append(console)
if push_config.get("DD_BOT_TOKEN") and push_config.get("DD_BOT_SECRET"):
notify_function.append(dingding_bot)
if push_config.get("FSKEY"):
notify_function.append(feishu_bot)
if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"):
notify_function.append(go_cqhttp)
if push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"):
notify_function.append(gotify)
if push_config.get("IGOT_PUSH_KEY"):
notify_function.append(iGot)
if push_config.get("PUSH_KEY"):
notify_function.append(serverJ)
if push_config.get("DEER_KEY"):
notify_function.append(pushdeer)
if push_config.get("CHAT_URL") and push_config.get("CHAT_TOKEN"):
notify_function.append(chat)
if push_config.get("PUSH_PLUS_TOKEN"):
notify_function.append(pushplus_bot)
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
notify_function.append(qmsg_bot)
if push_config.get("QYWX_AM"):
notify_function.append(wecom_app)
if push_config.get("QYWX_KEY"):
notify_function.append(wecom_bot)
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
notify_function.append(telegram_bot)
if (
push_config.get("AIBOTK_KEY")
and push_config.get("AIBOTK_TYPE")
and push_config.get("AIBOTK_NAME")
):
notify_function.append(aibotk)
if (
push_config.get("SMTP_SERVER")
and push_config.get("SMTP_SSL")
and push_config.get("SMTP_EMAIL")
and push_config.get("SMTP_PASSWORD")
and push_config.get("SMTP_NAME")
):
notify_function.append(smtp)
if push_config.get("PUSHME_KEY"):
notify_function.append(pushme)
if (
push_config.get("CHRONOCAT_URL")
and push_config.get("CHRONOCAT_QQ")
and push_config.get("CHRONOCAT_TOKEN")
):
notify_function.append(chronocat)
if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"):
notify_function.append(custom_notify)
def add_notify_function():
if push_config.get("BARK_PUSH"):
notify_function.append(bark)
if push_config.get("CONSOLE"):
notify_function.append(console)
if push_config.get("DD_BOT_TOKEN") and push_config.get("DD_BOT_SECRET"):
notify_function.append(dingding_bot)
if push_config.get("FSKEY"):
notify_function.append(feishu_bot)
if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"):
notify_function.append(go_cqhttp)
if push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"):
notify_function.append(gotify)
if push_config.get("IGOT_PUSH_KEY"):
notify_function.append(iGot)
if push_config.get("PUSH_KEY"):
notify_function.append(serverJ)
if push_config.get("DEER_KEY"):
notify_function.append(pushdeer)
if push_config.get("CHAT_URL") and push_config.get("CHAT_TOKEN"):
notify_function.append(chat)
if push_config.get("PUSH_PLUS_TOKEN"):
notify_function.append(pushplus_bot)
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
notify_function.append(qmsg_bot)
if push_config.get("QYWX_AM"):
notify_function.append(wecom_app)
if push_config.get("QYWX_KEY"):
notify_function.append(wecom_bot)
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
notify_function.append(telegram_bot)
if (
push_config.get("AIBOTK_KEY")
and push_config.get("AIBOTK_TYPE")
and push_config.get("AIBOTK_NAME")
):
notify_function.append(aibotk)
if (
push_config.get("SMTP_SERVER")
and push_config.get("SMTP_SSL")
and push_config.get("SMTP_EMAIL")
and push_config.get("SMTP_PASSWORD")
and push_config.get("SMTP_NAME")
):
notify_function.append(smtp)
if push_config.get("PUSHME_KEY"):
notify_function.append(pushme)
if (
push_config.get("CHRONOCAT_URL")
and push_config.get("CHRONOCAT_QQ")
and push_config.get("CHRONOCAT_TOKEN")
):
notify_function.append(chronocat)
if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"):
notify_function.append(custom_notify)
def send(title: str, content: str) -> None:
@@ -910,10 +909,9 @@ def send(title: str, content: str) -> None:
return
hitokoto = push_config.get("HITOKOTO")
content += "\n\n" + one() if hitokoto else ""
text = one() if hitokoto else ""
content += "\n\n" + text
add_notify_function()
ts = [
threading.Thread(target=mode, args=(title, content), name=mode.__name__)
for mode in notify_function
-12
View File
@@ -1,12 +0,0 @@
{
"name": "dependence",
"author": "",
"license": "ISC",
"dependencies": {
"got": "^11.5.1",
"nodemailer": "^6.8.0",
"tough-cookie": "^4.0.0",
"tunnel": "0.0.6",
"ws": "^7.4.3"
}
}
+4 -4
View File
@@ -49,7 +49,7 @@ add_cron_api() {
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
--compressed
)
code=$(echo "$api" | jq -r .code)
@@ -85,7 +85,7 @@ update_cron_api() {
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo "$api" | jq -r .code)
@@ -117,7 +117,7 @@ update_cron_command_api() {
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
--data-raw "{\"command\":\"${command//\"/\\\"}\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo "$api" | jq -r .code)
@@ -196,7 +196,7 @@ notify_api() {
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
--data-raw "{\"title\":\"${title//\"/\\\"}\",\"content\":\"${content//\"/\\\"}\"}" \
--compressed
)
code=$(echo "$api" | jq -r .code)
-3
View File
@@ -25,9 +25,6 @@ fi
echo -e "\nbot文件下载成功...\n"
echo -e "3、安装python3依赖...\n"
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
cd $dir_data
+1 -13
View File
@@ -9,9 +9,6 @@ reset_env() {
npm_install_2 $dir_root
echo -e "---> 青龙依赖安装完成\n"
echo -e "---> 3. 开始安装脚本依赖\n"
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
echo -e "---> 脚本依赖安装完成\n"
}
@@ -76,18 +73,9 @@ check_pm2() {
main() {
echo -e "=====> 开始检测"
npm i -g pnpm@8.3.1
npm i -g pnpm@8.3.1 pm2 tsx
patch_version
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
cd && pnpm config set registry $NpmMirror
pnpm install -g
fi
pnpm add -g pm2 tsx
reset_env
copy_dep
check_ql
Executable
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
store_env_vars() {
initial_vars=($(env | cut -d= -f1))
}
restore_env_vars() {
for key in $(env | cut -d= -f1); do
if ! [[ " ${initial_vars[@]} " =~ " $key " ]]; then
unset "$key"
fi
done
}
store_env_vars
+4 -6
View File
@@ -19,7 +19,6 @@ dir_update_log=$dir_log/update
ql_static_repo=$dir_repo/static
## 文件
file_ecosystem_js=$dir_root/ecosystem.config.js
file_config_sample=$dir_sample/config.sample.sh
file_env=$dir_config/env.sh
file_sharecode=$dir_config/sharecode.sh
@@ -71,8 +70,8 @@ import_config() {
ql_base_url=${QlBaseUrl:-"/"}
ql_port=${QlPort:-"5700"}
command_timeout_time=${CommandTimeoutTime:-""}
proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"}
proxy_url=${ProxyUrl:-""}
current_branch=${QL_BRANCH}
if [[ -n "${DefaultCronRule}" ]]; then
@@ -294,7 +293,7 @@ git_clone_scripts() {
set_proxy "$proxy"
git clone --depth=1 $part_cmd $url $dir 2>&1
git clone --depth=1 $part_cmd $url $dir
exit_status=$?
unset_proxy
@@ -308,8 +307,9 @@ random_range() {
reload_pm2() {
cd $dir_root
restore_env_vars
pm2 flush &>/dev/null
env ALL_PROXY= HTTP_PROXY= HTTPS_PROXY= all_proxy= http_proxy= https_proxy= pm2 startOrGracefulReload $file_ecosystem_js --update-env
pm2 startOrGracefulReload ecosystem.config.js
}
diff_time() {
@@ -361,8 +361,6 @@ format_timestamp() {
patch_version() {
git config --global pull.rebase false
cp -f $dir_root/.env.example $dir_root/.env
if [[ -f "$dir_root/db/cookie.db" ]]; then
echo -e "检测到旧的db文件,拷贝为新db...\n"
mv $dir_root/db/cookie.db $dir_root/db/env.db
+12 -19
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
dir_shell=$QL_DIR/shell
. $dir_shell/env.sh
. $dir_shell/share.sh
. $dir_shell/api.sh
@@ -137,7 +138,7 @@ update_repo() {
echo -e "\n拉取 ${uniq_path} 成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" "$autoAddCron" "$autoDelCron"
else
echo -e "\n拉取 ${uniq_path} 失败,请检查网络...\n"
echo -e "\n拉取 ${uniq_path} 失败,请检查日志...\n"
fi
}
@@ -244,6 +245,7 @@ reload_qinglong() {
fi
if [[ "$reload_target" == 'data' ]]; then
rm -rf ${dir_root}/data
cp -rf ${dir_tmp}/data ${dir_root}/
fi
@@ -265,8 +267,8 @@ update_qinglong() {
echo -e "使用 ${mirror} 源更新...\n"
local primary_branch="master"
if [[ "${QL_BRANCH}" == "develop" ]]; then
primary_branch="develop"
if [[ "${QL_BRANCH}" == "develop" ]] || [[ "${QL_BRANCH}" == "debian" ]] || [[ "${QL_BRANCH}" == "debian-dev" ]]; then
primary_branch="${QL_BRANCH}"
fi
wget -cqO "${dir_tmp}/ql.zip" "${downloadQLUrl}/${primary_branch}.zip"
@@ -299,11 +301,6 @@ update_qinglong_static() {
check_update_dep() {
echo -e "\n开始检测依赖...\n"
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
fi
if [[ $(diff $dir_root/package.json ${dir_tmp}/qinglong-${primary_branch}/package.json) ]]; then
npm_install_2 "${dir_tmp}/qinglong-${primary_branch}"
fi
@@ -475,8 +472,13 @@ main() {
local log_path="${log_dir}/${log_time}.log"
local file_path="$dir_log/$log_path"
cmd=">> $file_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
cmd="2>&1 | tee -a $file_path"
if [[ "$no_tee" == "true" ]]; then
cmd=">> $file_path 2>&1"
fi
if [[ "$real_time" == "true" ]]; then
cmd=""
fi
local time_format="%Y-%m-%d %H:%M:%S"
local time=$(date "+$time_format")
@@ -489,11 +491,6 @@ main() {
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
fi
if [[ "$show_log" == "true" ]] && [[ $ID ]]; then
eval echo -e "请移除 -l 参数" $cmd
exit 1
fi
case $p1 in
update)
fix_config
@@ -558,10 +555,6 @@ main() {
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒     " $cmd
fi
if [[ -f $file_path ]]; then
cat $file_path
fi
}
main "$@"
+1 -1
View File
@@ -1,7 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({
scriptUrl: ['//at.alicdn.com/t/c/font_3354854_ob5y15ewlyq.js'],
scriptUrl: ['//at.alicdn.com/t/c/font_3354854_lc939gab1iq.js'],
});
export default IconFont;
+18 -4
View File
@@ -100,12 +100,14 @@
"删除中": "Deleting",
"已删除": "Deleted",
"删除失败": "Deletion Failed",
"已取消": "Cancelled",
"序号": "Number",
"备注": "Remarks",
"更新时间": "Update Time",
"创建时间": "Creation Time",
"确认删除依赖": "Confirm to delete the dependency",
"确认重新安装": "Confirm to reinstall",
"确认取消安装": "Confirm to cancel install",
"确认删除选中的依赖吗": "Confirm to delete the selected dependencies?",
"确认重新安装选中的依赖吗": "Confirm to reinstall the selected dependencies?",
"请输入名称": "Please enter a name",
@@ -141,7 +143,7 @@
"服务启动超时": "Service startup timeout",
"请先按如下方式修复:": "Please fix it as follows:",
"1. 宿主机执行 docker run --rm -v\n /var/run/docker.sock:/var/run/docker.sock\n containrrr/watchtower -cR <容器名>": "1. Execute 'docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR <container_name>' on the host machine",
"2. 容器内执行 ql -l check、ql -l update": "2. Execute 'ql -l check' and 'ql -l update' inside the container",
"2. 容器内执行 ql check、ql update": "2. Execute 'ql check' and 'ql update' inside the container",
"3. 如果无法解决,容器内执行 pm2 logs,拷贝执行结果": "3. If the problem persists, execute 'pm2 logs' inside the container and copy the results",
"提交 issue": "Submit an issue",
"启动中,请稍后...": "Starting, please wait...",
@@ -237,6 +239,7 @@
"系统将在": "The system will restart in",
"秒后自动刷新": "seconds and automatically refresh",
"检查更新": "Check for Updates",
"重新启动": "Reboot",
"确认删除应用": "Confirm to delete the application",
"确认重置": "Confirm to reset",
"确认重置应用": "Confirm to reset the application",
@@ -375,9 +378,9 @@
"微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/": "After WeChat scan login, one-to-one or one-to-many push using the provided token (your Token). If PUSH_PLUS_USER is not provided, it defaults to one-to-one push. See reference at https://www.pushplus.plus/",
"一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)": "The 'group code' for one-to-many push (one-to-many push -> your group (if none, create one) -> group code). If you are the creator of the group, you need to click 'View QR code' to scan and bind, otherwise, you won't receive group messages.",
"飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973": "Feishu group bot: https://www.feishu.cn/hc/zh-CN/articles/360024984973",
"邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://nodemailer.com/smtp/well-known/": "Email service name, e.g., 126, 163, Gmail, QQ, etc. Supported list: https://nodemailer.com/smtp/well-known/",
"邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json": "Email service name, e.g., 126, 163, Gmail, QQ, etc. Supported list: https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json",
"邮箱地址": "Email Address",
"邮箱SMTP授权码": "Email SMTP Authorization Code",
"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/",
"请求方法": "Request Method",
"请求头Content-Type": "Request Header Content-Type",
@@ -393,6 +396,7 @@
"系统": "System",
"个人": "Personal",
"重新安装": "Reinstall",
"取消安装": "Cancel Install",
"强制删除": "Force Delete",
"全部任务": "All Tasks",
"关联订阅": "Associate Subscription",
@@ -466,5 +470,15 @@
"个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx": "Individuals: user_id=individual QQ Groups fill in group_id=QQ Groups more than one with English; separated by the same time to support individuals and groups such as: user_id=xxx;group_id=xxxx;group_id=xxxxx",
"docker安装在持久化config目录下的chronocat.yml文件可找到": "The docker installation can be found in the persistence config directory in the chronocat.yml file",
"请选择": "Please select",
"请输入": "Please input"
"请输入": "Please input",
"依赖设置": "Dependence Settings",
"Node 软件包镜像源": "Node Software Package Mirror Source",
"Python 软件包镜像源": "Python Software Package Mirror Source",
"Linux 软件包镜像源": "Linux Software Package Mirror Source",
"代理与镜像源二选一即可": "Either Proxy or Mirror Source can be chosen",
"代理地址, 支持HTTP(S)/SOCK5": "Proxy Address, supports HTTP(S)/SOCK5",
"NPM 镜像源": "NPM Mirror Source",
"PyPI 镜像源": "PyPI Mirror Source",
"alpine linux 镜像源": "Alpine Linux Mirror Source",
"如果恢复失败,可进入容器执行": "If recovery fails, you can enter the container and execute"
}
+18 -4
View File
@@ -100,12 +100,14 @@
"删除中": "删除中",
"已删除": "已删除",
"删除失败": "删除失败",
"已取消": "已取消",
"序号": "序号",
"备注": "备注",
"更新时间": "更新时间",
"创建时间": "创建时间",
"确认删除依赖": "确认删除依赖",
"确认重新安装": "确认重新安装",
"确认取消安装": "确认取消安装",
"确认删除选中的依赖吗": "确认删除选中的依赖吗",
"确认重新安装选中的依赖吗": "确认重新安装选中的依赖吗",
"请输入名称": "请输入名称",
@@ -141,7 +143,7 @@
"服务启动超时": "服务启动超时",
"请先按如下方式修复:": "请先按如下方式修复:",
"1. 宿主机执行 docker run --rm -v\n /var/run/docker.sock:/var/run/docker.sock\n containrrr/watchtower -cR <容器名>": "1. 宿主机执行 docker run --rm -v\n /var/run/docker.sock:/var/run/docker.sock\n containrrr/watchtower -cR <容器名>",
"2. 容器内执行 ql -l check、ql -l update": "2. 容器内执行 ql -l check、ql -l update",
"2. 容器内执行 ql check、ql update": "2. 容器内执行 ql check、ql update",
"3. 如果无法解决,容器内执行 pm2 logs,拷贝执行结果": "3. 如果无法解决,容器内执行 pm2 logs,拷贝执行结果",
"提交 issue": "提交 issue",
"启动中,请稍后...": "启动中,请稍后...",
@@ -237,6 +239,7 @@
"系统将在": "系统将在",
"秒后自动刷新": "秒后自动刷新",
"检查更新": "检查更新",
"重新启动": "重新启动",
"确认删除应用": "确认删除应用",
"确认重置": "确认重置",
"确认重置应用": "确认重置应用",
@@ -375,9 +378,9 @@
"微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/": "微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/",
"一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)": "一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)",
"飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973": "飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973",
"邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://nodemailer.com/smtp/well-known/": "邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://nodemailer.com/smtp/well-known/",
"邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json": "邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json",
"邮箱地址": "邮箱地址",
"邮箱SMTP授权码": "邮箱SMTP授权码",
"SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定": "SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定",
"PushMe的Keyhttps://push.i-i.me/": "PushMe的Keyhttps://push.i-i.me/",
"请求方法": "请求方法",
"请求头Content-Type": "请求头Content-Type",
@@ -393,6 +396,7 @@
"系统": "系统",
"个人": "个人",
"重新安装": "重新安装",
"取消安装": "取消安装",
"强制删除": "强制删除",
"全部任务": "全部任务",
"关联订阅": "关联订阅",
@@ -466,5 +470,15 @@
"个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx": "个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx",
"docker安装在持久化config目录下的chronocat.yml文件可找到": "docker安装在持久化config目录下的chronocat.yml文件可找到",
"请选择": "请选择",
"请输入": "请输入"
"请输入": "请输入",
"依赖设置": "依赖设置",
"Node 软件包镜像源": "Node 软件包镜像源",
"Python 软件包镜像源": "Python 软件包镜像源",
"Linux 软件包镜像源": "Linux 软件包镜像源",
"代理与镜像源二选一即可": "代理与镜像源二选一即可",
"代理地址, 支持HTTP(S)/SOCK5": "代理地址, 支持HTTP(S)/SOCK5",
"NPM 镜像源": "NPM 镜像源",
"PyPI 镜像源": "PyPI 镜像源",
"alpine linux 镜像源": "alpine linux 镜像源",
"如果恢复失败,可进入容器执行": "如果恢复失败,可进入容器执行"
}
+22 -7
View File
@@ -1,4 +1,4 @@
import intl from 'react-intl-universal'
import intl from 'react-intl-universal';
import React, {
PureComponent,
Fragment,
@@ -15,6 +15,8 @@ import CodeMirror from '@uiw/react-codemirror';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import { langs } from '@uiw/codemirror-extensions-langs';
import { useHotkeys } from 'react-hotkeys-hook';
import { getEditorMode } from '@/utils';
const Config = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
@@ -25,13 +27,16 @@ const Config = () => {
const [data, setData] = useState<any[]>([]);
const editorRef = useRef<any>(null);
const [confirmLoading, setConfirmLoading] = useState(false);
const [language, setLanguage] = useState<string>('shell');
const getConfig = (name: string) => {
request.get(`${config.apiPrefix}configs/${name}`).then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
});
request
.get(`${config.apiPrefix}configs/detail?path=${encodeURIComponent(name)}`)
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
});
};
const getFiles = () => {
@@ -66,8 +71,18 @@ const Config = () => {
setSelect(value);
setTitle(node.value);
getConfig(node.value);
const newMode = getEditorMode(value);
setLanguage(newMode);
};
useHotkeys(
'mod+s',
(e) => {
updateConfig();
},
{ enableOnFormTags: ['textarea'], preventDefault: true },
);
useEffect(() => {
getFiles();
getConfig('config.sh');
@@ -113,7 +128,7 @@ const Config = () => {
/>
) : (
<Editor
defaultLanguage="shell"
language={language}
value={value}
theme={theme}
options={{
+16 -16
View File
@@ -107,10 +107,9 @@ const CronDetailModal = ({
value={value}
options={{
fontSize: 12,
minimap: { enabled: false },
lineNumbersMinChars: 3,
fontFamily: 'Source Code Pro',
glyphMargin: false,
wordWrap: 'on',
}}
onMount={(editor, monaco) => {
editorRef.current = editor;
@@ -120,21 +119,17 @@ const CronDetailModal = ({
};
const onClickItem = (item: LogItem) => {
const url = `${config.apiPrefix}logs/${item.filename}?path=${
const url = `${config.apiPrefix}logs/detail?file=${item.filename}&path=${
item.directory || ''
}`;
localStorage.setItem('logCron', url);
setLogUrl(url);
request
.get(
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
)
.then(({ code, data }) => {
if (code === 200) {
setLog(data);
setIsLogModalVisible(true);
}
});
request.get(url).then(({ code, data }) => {
if (code === 200) {
setLog(data);
setIsLogModalVisible(true);
}
});
};
const onTabChange = (key: string) => {
@@ -160,7 +155,7 @@ const CronDetailModal = ({
const [s, p] = result;
setScriptInfo({ parent: p, filename: s });
request
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
.get(`${config.apiPrefix}scripts/detail?file=${s}&path=${p || ''}`)
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
@@ -362,8 +357,13 @@ const CronDetailModal = ({
<Modal
title={
<div className="crontab-title-wrapper">
<div>
<span>{currentCron.name}</span>
<div style={{ minWidth: 0 }}>
<Typography.Text
style={{ width: '100%' }}
ellipsis={{ tooltip: currentCron.name }}
>
{currentCron.name}
</Typography.Text>
{currentCron.labels?.length > 0 && currentCron.labels[0] !== '' && (
<Divider type="vertical"></Divider>
)}
+7 -4
View File
@@ -6,9 +6,9 @@
.card-wrapper {
.ant-card:last-child {
.ant-card-body {
min-height: 300px;
height: calc(90vh - 367px);
height: calc(90vh - var(--vh-offset, 0px) - 367px);
min-height: 0;
height: calc(80vh - 314px);
height: calc(80vh - var(--vh-offset, 0px) - 314px);
overflow-y: auto;
> div {
@@ -21,6 +21,9 @@
.ant-modal-body {
background: #eee;
padding: 12px;
max-height: calc(80vh - 57px);
max-height: calc(80vh - var(--vh-offset, 57px));
word-wrap: unset;
}
.ant-card-body {
@@ -195,4 +198,4 @@ body[data-mode='desktop'] {
.cron.pinned-cron > td {
background: #f2f2f2;
}
}
+111 -38
View File
@@ -22,6 +22,7 @@ import {
FileTextOutlined,
CloseCircleOutlined,
ClockCircleOutlined,
MinusCircleOutlined,
} from '@ant-design/icons';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
@@ -36,20 +37,12 @@ import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import dayjs from 'dayjs';
import WebSocketManager from '@/utils/websocket';
import { DependenceStatus, Status } from './type';
import IconFont from '@/components/iconfont';
const { Text } = Typography;
const { Search } = Input;
enum Status {
'安装中',
'已安装',
'安装失败',
'删除中',
'已删除',
'删除失败',
'队列中',
}
enum StatusColor {
'processing',
'success',
@@ -85,6 +78,10 @@ const StatusMap: Record<number, { icon: React.ReactNode; color: string }> = {
icon: <ClockCircleOutlined />,
color: 'default',
},
7: {
icon: <MinusCircleOutlined />,
color: 'default',
},
};
const Dependence = () => {
@@ -108,8 +105,41 @@ const Dependence = () => {
key: 'status',
width: 120,
dataIndex: 'status',
filters: [
{
text: intl.get('队列中'),
value: DependenceStatus.queued,
},
{
text: intl.get('安装中'),
value: DependenceStatus.installing,
},
{
text: intl.get('已安装'),
value: DependenceStatus.installed,
},
{
text: intl.get('安装失败'),
value: DependenceStatus.installFailed,
},
{
text: intl.get('删除中'),
value: DependenceStatus.removing,
},
{
text: intl.get('已删除'),
value: DependenceStatus.removed,
},
{
text: intl.get('删除失败'),
value: DependenceStatus.removeFailed,
},
{
text: intl.get('已取消'),
value: DependenceStatus.cancelled,
},
],
render: (text: string, record: any, index: number) => {
console.log(record.status);
return (
<Space size="middle" style={{ cursor: 'text' }}>
<Tag
@@ -155,35 +185,46 @@ const Dependence = () => {
const isPc = !isPhone;
return (
<Space size="middle">
<Tooltip title={isPc ? intl.get('日志') : ''}>
<a
onClick={() => {
setLogDependence({ ...record, timestamp: Date.now() });
}}
>
<FileTextOutlined />
</a>
</Tooltip>
{record.status !== Status. &&
record.status !== Status. && (
<>
<Tooltip title={isPc ? intl.get('重新安装') : ''}>
<a onClick={() => reInstallDependence(record, index)}>
<BugOutlined />
</a>
</Tooltip>
{![Status., Status.].includes(record.status) && (
<Tooltip title={isPc ? intl.get('日志') : ''}>
<a
onClick={() => {
setLogDependence({ ...record, timestamp: Date.now() });
}}
>
<FileTextOutlined />
</a>
</Tooltip>
)}
{[Status., Status., Status.].includes(
record.status,
) ? (
<Tooltip title={isPc ? intl.get('取消安装') : ''}>
<a onClick={() => cancelDependence(record)}>
<IconFont type="ql-icon-quxiaoanzhuang" />
</a>
</Tooltip>
) : (
<>
<Tooltip title={isPc ? intl.get('重新安装') : ''}>
<a onClick={() => reInstallDependence(record, index)}>
<BugOutlined />
</a>
</Tooltip>
{Status. === record.status && (
<Tooltip title={isPc ? intl.get('删除') : ''}>
<a onClick={() => deleteDependence(record, index)}>
<DeleteOutlined />
</a>
</Tooltip>
<Tooltip title={isPc ? intl.get('强制删除') : ''}>
<a onClick={() => deleteDependence(record, index, true)}>
<DeleteFilled />
</a>
</Tooltip>
</>
)}
)}
<Tooltip title={isPc ? intl.get('强制删除') : ''}>
<a onClick={() => deleteDependence(record, index, true)}>
<DeleteFilled />
</a>
</Tooltip>
</>
)}
</Space>
);
},
@@ -201,11 +242,15 @@ const Dependence = () => {
const tableRef = useRef<HTMLDivElement>(null);
const tableScrollHeight = useTableScrollHeight(tableRef, 59);
const getDependencies = () => {
const getDependencies = (status?: number[]) => {
setLoading(true);
request
.get(
`${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`,
`${
config.apiPrefix
}dependencies?searchValue=${searchText}&type=${type}&status=${
status || ''
}`,
)
.then(({ code, data }) => {
if (code === 200) {
@@ -290,6 +335,31 @@ const Dependence = () => {
});
};
const cancelDependence = (record: any) => {
Modal.confirm({
title: intl.get('确认取消安装'),
content: (
<>
{intl.get('确认取消安装')}{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
{intl.get('吗')}
</>
),
onOk() {
request
.put(`${config.apiPrefix}dependencies/cancel`, [record.id])
.then(() => {
getDependencies();
});
},
onCancel() {
console.log('Cancel');
},
});
};
const handleCancel = (dependence?: any[]) => {
setIsModalVisible(false);
dependence && handleDependence(dependence);
@@ -421,7 +491,7 @@ const Dependence = () => {
}
return _result;
});
}, 5000);
}, 300);
return;
}
}
@@ -539,6 +609,9 @@ const Dependence = () => {
size="middle"
scroll={{ x: 768, y: tableScrollHeight }}
loading={loading}
onChange={(pagination, filters) => {
getDependencies(filters?.status as number[]);
}}
/>
</DndProvider>
</div>
+9 -2
View File
@@ -10,6 +10,7 @@ import {
import { PageLoading } from '@ant-design/pro-layout';
import Ansi from 'ansi-to-react';
import WebSocketManager from '@/utils/websocket';
import { Status } from './type';
const DependenceLogModal = ({
dependence,
@@ -96,7 +97,11 @@ const DependenceLogModal = ({
const handleMessage = (payload: any) => {
const { message, references } = payload;
if (references.length > 0 && references.includes(dependence.id)) {
if (
references.length > 0 &&
references.includes(dependence.id) &&
[Status., Status.].includes(dependence.status)
) {
if (message.includes('结束时间')) {
setExecuting(false);
setIsRemoveFailed(message.includes('删除失败'));
@@ -108,11 +113,13 @@ const DependenceLogModal = ({
useEffect(() => {
const ws = WebSocketManager.getInstance();
ws.subscribe('installDependence', handleMessage);
ws.subscribe('uninstallDependence', handleMessage);
return () => {
ws.unsubscribe('installDependence', handleMessage);
ws.unsubscribe('uninstallDependence', handleMessage);
};
}, []);
}, [dependence]);
useEffect(() => {
setIsPhone(document.body.clientWidth < 768);
+21
View File
@@ -0,0 +1,21 @@
export enum DependenceStatus {
'installing',
'installed',
'installFailed',
'removing',
'removed',
'removeFailed',
'queued',
'cancelled',
}
export enum Status {
'安装中',
'已安装',
'安装失败',
'删除中',
'已删除',
'删除失败',
'队列中',
'已取消',
}
+13 -18
View File
@@ -9,22 +9,24 @@ import { DiffEditor } from '@monaco-editor/react';
import ReactDiffViewer from 'react-diff-viewer';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import { getEditorMode } from '@/utils';
const { Option } = Select;
const Diff = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [origin, setOrigin] = useState('config.sample.sh');
const [origin, setOrigin] = useState('sample/config.sample.sh');
const [current, setCurrent] = useState('config.sh');
const [originValue, setOriginValue] = useState('');
const [currentValue, setCurrentValue] = useState('');
const [loading, setLoading] = useState(true);
const [files, setFiles] = useState<any[]>([]);
const editorRef = useRef<any>(null);
const [language, setLanguage] = useState<string>('shell');
const getConfig = () => {
request
.get(`${config.apiPrefix}configs/${current}`)
.get(`${config.apiPrefix}configs/detail?path=${encodeURIComponent(current)}`)
.then(({ code, data }) => {
if (code === 200) {
setCurrentValue(data);
@@ -34,7 +36,7 @@ const Diff = () => {
const getSample = () => {
request
.get(`${config.apiPrefix}configs/${origin}`)
.get(`${config.apiPrefix}configs/detail?path=${encodeURIComponent(origin)}`)
.then(({ code, data }) => {
if (code === 200) {
setOriginValue(data);
@@ -62,7 +64,7 @@ const Diff = () => {
const getFiles = () => {
setLoading(true);
request
.get(`${config.apiPrefix}configs/files`)
.get(`${config.apiPrefix}configs/sample`)
.then(({ code, data }) => {
if (code === 200) {
setFiles(data);
@@ -71,12 +73,11 @@ const Diff = () => {
.finally(() => setLoading(false));
};
const originFileChange = (value: string) => {
const originFileChange = (value: string, op) => {
setCurrent(op.extra.target);
setOrigin(value);
};
const currentFileChange = (value: string) => {
setCurrent(value);
const newMode = getEditorMode(value);
setLanguage(newMode);
};
useEffect(() => {
@@ -112,7 +113,7 @@ const Diff = () => {
<Form.Item label={intl.get('源文件')}>
<Select value={origin} onChange={originFileChange}>
{files.map((x) => (
<Option key={x.value} value={x.value}>
<Option key={x.value} value={x.value} extra={x}>
{x.title}
</Option>
))}
@@ -121,13 +122,7 @@ const Diff = () => {
</Col>
<Col span={12}>
<Form.Item label={intl.get('当前文件')}>
<Select value={current} onChange={currentFileChange}>
{files.map((x) => (
<Option key={x.value} value={x.value}>
{x.title}
</Option>
))}
</Select>
<span className="ant-form-text">{current}</span>
</Form.Item>
</Col>
</Row>
@@ -159,7 +154,7 @@ const Diff = () => {
/>
) : (
<DiffEditor
language={'shell'}
language={language}
original={originValue}
modified={currentValue}
options={{
+1 -1
View File
@@ -71,7 +71,7 @@ const Error = () => {
/var/run/docker.sock:/var/run/docker.sock
containrrr/watchtower -cR &lt;&gt;
</div>
<div>{intl.get('2. 容器内执行 ql -l check、ql -l update')}</div>
<div>{intl.get('2. 容器内执行 ql check、ql update')}</div>
<div>
{intl.get(
'3. 如果无法解决,容器内执行 pm2 logs,拷贝执行结果'
+47 -47
View File
@@ -95,6 +95,53 @@ const Initialization = () => {
</div>
),
},
{
title: intl.get('通知设置'),
content: (
<Form onFinish={submitNotification} layout="vertical">
<Form.Item
label={intl.get('通知方式')}
name="type"
rules={[{ required: true, message: intl.get('请选择通知方式') }]}
style={{ maxWidth: 350 }}
>
<Select
onChange={notificationModeChange}
placeholder={intl.get('请选择通知方式')}
>
{config.notificationModes
.filter((x) => x.value !== 'closed')
.map((x) => (
<Option key={x.value} value={x.value}>
{x.label}
</Option>
))}
</Select>
</Form.Item>
{fields.map((x) => (
<Form.Item
key={x.label}
label={x.label}
name={x.label}
extra={x.tip}
rules={[{ required: x.required }]}
style={{ maxWidth: 400 }}
>
<Input.TextArea
autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={`请输入${x.label}`}
/>
</Form.Item>
))}
<Button type="primary" htmlType="submit" loading={loading}>
{intl.get('保存')}
</Button>
<Button type="link" htmlType="button" onClick={() => next()}>
{intl.get('跳过')}
</Button>
</Form>
),
},
{
title: intl.get('账户设置'),
content: (
@@ -152,53 +199,6 @@ const Initialization = () => {
</Form>
),
},
{
title: intl.get('通知设置'),
content: (
<Form onFinish={submitNotification} layout="vertical">
<Form.Item
label={intl.get('通知方式')}
name="type"
rules={[{ required: true, message: intl.get('请选择通知方式') }]}
style={{ maxWidth: 350 }}
>
<Select
onChange={notificationModeChange}
placeholder={intl.get('请选择通知方式')}
>
{config.notificationModes
.filter((x) => x.value !== 'closed')
.map((x) => (
<Option key={x.value} value={x.value}>
{x.label}
</Option>
))}
</Select>
</Form.Item>
{fields.map((x) => (
<Form.Item
key={x.label}
label={x.label}
name={x.label}
extra={x.tip}
rules={[{ required: x.required }]}
style={{ maxWidth: 400 }}
>
<Input.TextArea
autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={`请输入${x.label}`}
/>
</Form.Item>
))}
<Button type="primary" htmlType="submit" loading={loading}>
{intl.get('保存')}
</Button>
<Button type="link" htmlType="button" onClick={() => next()}>
{intl.get('跳过')}
</Button>
</Form>
),
},
{
title: intl.get('完成安装'),
content: (
+32 -10
View File
@@ -1,4 +1,4 @@
import intl from 'react-intl-universal'
import intl from 'react-intl-universal';
import { useState, useEffect, useCallback, Key, useRef } from 'react';
import {
TreeSelect,
@@ -25,13 +25,14 @@ import { depthFirstSearch } from '@/utils';
import debounce from 'lodash/debounce';
import uniq from 'lodash/uniq';
import useFilterTreeData from '@/hooks/useFilterTreeData';
import prettyBytes from 'pretty-bytes';
const { Text } = Typography;
const Log = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [value, setValue] = useState(intl.get('请选择日志文件'));
const [select, setSelect] = useState<string>('');
const [select, setSelect] = useState<string>(intl.get('请选择日志文件'));
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [height, setHeight] = useState<number>();
@@ -54,7 +55,11 @@ const Log = () => {
const getLog = (node: any) => {
request
.get(`${config.apiPrefix}logs/${node.title}?path=${node.parent || ''}`)
.get(
`${config.apiPrefix}logs/detail?file=${node.title}&path=${
node.parent || ''
}`,
)
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
@@ -63,13 +68,13 @@ const Log = () => {
};
const onSelect = (value: any, node: any) => {
setCurrentNode(node);
setSelect(value);
if (node.key === select || !value) {
return;
}
setCurrentNode(node);
setSelect(value);
if (node.type === 'directory') {
setValue(intl.get('请选择日志文件'));
return;
@@ -117,7 +122,8 @@ const Log = () => {
<Text style={{ wordBreak: 'break-all' }} type="warning">
{select}
</Text>
{intl.get('文件')}{currentNode.type === 'directory' ? intl.get('夹下所以日志') : ''}
{intl.get('文件')}
{currentNode.type === 'directory' ? intl.get('夹下所以日志') : ''}
{intl.get(',删除后不可恢复')}
</>
),
@@ -181,7 +187,24 @@ const Log = () => {
return (
<PageContainer
className="ql-container-wrapper log-wrapper"
title={select}
title={
<>
{select}
{currentNode?.type === 'file' && (
<span
style={{
marginLeft: 6,
fontSize: 12,
color: '#999',
display: 'inline-block',
height: 14,
}}
>
{prettyBytes(currentNode.size)}
</span>
)}
</>
}
loading={loading}
extra={
isPhone
@@ -266,11 +289,10 @@ const Log = () => {
options={{
readOnly: true,
fontSize: 12,
minimap: { enabled: false },
lineNumbersMinChars: 3,
fontFamily: 'Source Code Pro',
folding: false,
glyphMargin: false,
wordWrap: 'on',
}}
/>
</SplitPane>
+8 -3
View File
@@ -36,7 +36,7 @@ const EditModal = ({
const [value, setValue] = useState('');
const [language, setLanguage] = useState<string>('javascript');
const [cNode, setCNode] = useState<any>();
const [selectedKey, setSelectedKey] = useState<string>('');
const [selectedKey, setSelectedKey] = useState<string>();
const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false);
const [settingModalVisible, setSettingModalVisible] =
useState<boolean>(false);
@@ -67,7 +67,11 @@ const EditModal = ({
const getDetail = (node: any) => {
request
.get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`)
.get(
`${config.apiPrefix}scripts/detail?file=${node.title}&path=${
node.parent || ''
}`,
)
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
@@ -148,7 +152,7 @@ const EditModal = ({
<>
<TreeSelect
treeExpandAction="click"
style={{ marginRight: 8, width: 150 }}
style={{ marginRight: 8, width: 300 }}
value={selectedKey}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={treeData}
@@ -156,6 +160,7 @@ const EditModal = ({
fieldNames={{ value: 'key', label: 'title' }}
showSearch
onSelect={onSelect}
treeLine={{ showLeafIcon: true }}
/>
<Select
value={language}
+94 -22
View File
@@ -44,14 +44,14 @@ import uniq from 'lodash/uniq';
import IconFont from '@/components/iconfont';
import RenameModal from './renameModal';
import { langs } from '@uiw/codemirror-extensions-langs';
import { useHotkeys } from 'react-hotkeys-hook';
import prettyBytes from 'pretty-bytes';
const { Text } = Typography;
const Script = () => {
const { headerStyle, isPhone, theme } =
useOutletContext<SharedContext>();
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [value, setValue] = useState(intl.get('请选择脚本文件'));
const [select, setSelect] = useState<string>('');
const [select, setSelect] = useState<string>(intl.get('请选择脚本文件'));
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [mode, setMode] = useState('');
@@ -84,9 +84,9 @@ const Script = () => {
const getDetail = (node: any) => {
request
.get(
`${config.apiPrefix}scripts/${encodeURIComponent(node.title)}?path=${
node.parent || ''
}`,
`${config.apiPrefix}scripts/detail?file=${encodeURIComponent(
node.title,
)}&path=${node.parent || ''}`,
)
.then(({ code, data }) => {
if (code === 200) {
@@ -115,13 +115,13 @@ const Script = () => {
};
const onSelect = (value: any, node: any) => {
setSelect(node.key);
setCurrentNode(node);
if (node.key === select || !value) {
return;
}
setSelect(node.key);
setCurrentNode(node);
if (node.type === 'directory') {
setValue(intl.get('请选择脚本文件'));
return;
@@ -135,6 +135,10 @@ const Script = () => {
const onTreeSelect = useCallback(
(keys: Key[], e: any) => {
const node = e.node;
if (node.key === select && isEditing) {
return;
}
const content = editorRef.current
? editorRef.current.getValue().replace(/\r\n/g, '\n')
: value;
@@ -155,7 +159,7 @@ const Script = () => {
onSelect(keys[0], e.node);
}
},
[value],
[value, select, isEditing],
);
const onSearch = useCallback(
@@ -187,6 +191,14 @@ const Script = () => {
setExpandedKeys(expKeys);
};
const onDoubleClick = (e: any, node: any) => {
if (node.type === 'file') {
setSelect(node.key);
setCurrentNode(node);
setIsEditing(true);
}
};
const editFile = () => {
setTimeout(() => {
setIsEditing(true);
@@ -352,7 +364,7 @@ const Script = () => {
};
const initState = () => {
setSelect('');
setSelect(intl.get('请选择脚本文件'));
setCurrentNode(null);
setValue(intl.get('请选择脚本文件'));
};
@@ -367,6 +379,46 @@ const Script = () => {
}
}, [treeDom.current, data]);
useHotkeys(
'mod+s',
(e) => {
if (isEditing) {
saveFile();
}
},
{ enableOnFormTags: ['textarea'], preventDefault: true },
);
useHotkeys(
'mod+d',
(e) => {
if (currentNode.title) {
deleteFile();
}
},
{ preventDefault: true },
);
useHotkeys(
'mod+o',
(e) => {
if (!isEditing) {
addFile();
}
},
{ preventDefault: true },
);
useHotkeys(
'mod+e',
(e) => {
if (currentNode.title) {
cancelEdit();
}
},
{ preventDefault: true },
);
const action = (key: string | number) => {
switch (key) {
case 'save':
@@ -441,7 +493,24 @@ const Script = () => {
return (
<PageContainer
className="ql-container-wrapper log-wrapper"
title={select}
title={
<>
{select}
{currentNode?.type === 'file' && (
<span
style={{
marginLeft: 6,
fontSize: 12,
color: '#999',
display: 'inline-block',
height: 14,
}}
>
{prettyBytes(currentNode.size)}
</span>
)}
</>
}
loading={loading}
extra={
isPhone
@@ -543,6 +612,7 @@ const Script = () => {
onExpand={onExpand}
showLine={{ showLeafIcon: true }}
onSelect={onTreeSelect}
onDoubleClick={onDoubleClick}
></Tree>
</div>
</>
@@ -591,15 +661,17 @@ const Script = () => {
}}
/>
)}
{isLogModalVisible && <EditModal
visible={isLogModalVisible}
treeData={data}
currentNode={currentNode}
content={value}
handleCancel={() => {
setIsLogModalVisible(false);
}}
/>}
{isLogModalVisible && (
<EditModal
visible={isLogModalVisible}
treeData={data}
currentNode={currentNode}
content={value}
handleCancel={() => {
setIsLogModalVisible(false);
}}
/>
)}
<EditScriptNameModal
visible={isAddFileModalVisible}
treeData={data}
+6 -1
View File
@@ -62,7 +62,12 @@ const RenameModal = ({
onCancel={() => handleCancel()}
confirmLoading={loading}
>
<Form form={form} layout="vertical" name="edit_name_modal">
<Form
form={form}
initialValues={{ name: currentNode?.title }}
layout="vertical"
name="edit_name_modal"
>
<Form.Item
name="name"
rules={[{ required: true, message: intl.get('请输入新名称') }]}
+2
View File
@@ -26,6 +26,8 @@ const SaveModal = ({
message.success(intl.get('保存文件成功'));
handleCancel(data);
}
})
.finally(() => {
setLoading(false);
});
};
+45 -26
View File
@@ -77,7 +77,11 @@ const CheckUpdate = ({ systemInfo }: any) => {
</div>
</>
),
content: <pre><Ansi>{lastLog}</Ansi></pre>,
content: (
<pre>
<Ansi>{lastLog}</Ansi>
</pre>
),
okText: intl.get('下载更新'),
cancelText: intl.get('以后再说'),
onOk() {
@@ -102,10 +106,41 @@ const CheckUpdate = ({ systemInfo }: any) => {
okButtonProps: { disabled: true },
title: intl.get('下载更新中...'),
centered: true,
content: <pre><Ansi>{value}</Ansi></pre>,
content: (
<pre>
<Ansi>{value}</Ansi>
</pre>
),
});
};
const reloadSystem = (type?: string) => {
request
.put(`${config.apiPrefix}system/reload`, { type })
.then((_data: any) => {
message.success({
content: (
<span>
{intl.get('系统将在')}
<Countdown
className="inline-countdown"
format="ss"
value={Date.now() + 1000 * 30}
/>
{intl.get('秒后自动刷新')}
</span>
),
duration: 30,
});
setTimeout(() => {
window.location.reload();
}, 30000);
})
.catch((error: any) => {
console.log(error);
});
};
const showReloadModal = () => {
Modal.confirm({
width: 600,
@@ -115,30 +150,7 @@ const CheckUpdate = ({ systemInfo }: any) => {
content: intl.get('系统安装包下载成功,确认重启'),
okText: intl.get('重启'),
onOk() {
request
.put(`${config.apiPrefix}system/reload`, { type: 'system' })
.then((_data: any) => {
message.success({
content: (
<span>
{intl.get('系统将在')}
<Countdown
className="inline-countdown"
format="ss"
value={Date.now() + 1000 * 30}
/>
{intl.get('秒后自动刷新')}
</span>
),
duration: 30,
});
setTimeout(() => {
window.location.reload();
}, 30000);
})
.catch((error: any) => {
console.log(error);
});
reloadSystem('system');
},
onCancel() {
modalRef.current.update({
@@ -206,6 +218,13 @@ const CheckUpdate = ({ systemInfo }: any) => {
<Button type="primary" onClick={checkUpgrade}>
{intl.get('检查更新')}
</Button>
<Button
type="primary"
onClick={() => reloadSystem()}
style={{ marginLeft: 8 }}
>
{intl.get('重新启动')}
</Button>
</>
);
};
+239
View File
@@ -0,0 +1,239 @@
import intl from 'react-intl-universal';
import React, { useState, useEffect, useRef } from 'react';
import { Button, InputNumber, Form, message, Input, Alert } from 'antd';
import config from '@/utils/config';
import { request } from '@/utils/http';
import './index.less';
import Ansi from 'ansi-to-react';
import pick from 'lodash/pick';
import WebSocketManager from '@/utils/websocket';
const dataMap = {
'dependence-proxy': 'dependenceProxy',
'node-mirror': 'nodeMirror',
'python-mirror': 'pythonMirror',
'linux-mirror': 'linuxMirror',
};
const Dependence = () => {
const [systemConfig, setSystemConfig] = useState<{
dependenceProxy?: string;
nodeMirror?: string;
pythonMirror?: string;
linuxMirror?: string;
}>();
const [form] = Form.useForm();
const [log, setLog] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const getSystemConfig = () => {
request
.get(`${config.apiPrefix}system/config`)
.then(({ code, data }) => {
if (code === 200 && data.info) {
setSystemConfig(data.info);
}
})
.catch((error: any) => {
console.log(error);
});
};
const updateSystemConfigStream = (path: keyof typeof dataMap) => {
setLoading(true);
setLog('in progress...\n');
request
.put<string>(
`${config.apiPrefix}system/config/${path}`,
pick(systemConfig, dataMap[path]),
)
.then((res) => {})
.catch((error: any) => {
console.log(error);
});
};
const updateSystemConfig = (path: keyof typeof dataMap) => {
setLoading(true);
setLog('');
request
.put(
`${config.apiPrefix}system/config/${path}`,
pick(systemConfig, dataMap[path]),
)
.then(({ code, data }) => {
if (code === 200) {
message.success(intl.get('更新成功'));
}
})
.catch((error: any) => {
console.log(error);
})
.finally(() => setLoading(false));
};
const handleMessage = (payload: any) => {
const { message } = payload;
setLog((p) => `${p}${message}`);
if (
message.includes('update node mirror end') ||
message.includes('update linux mirror end')
) {
setLoading(false);
}
};
useEffect(() => {
const ws = WebSocketManager.getInstance();
ws.subscribe('updateNodeMirror', handleMessage);
ws.subscribe('updateLinuxMirror', handleMessage);
return () => {
ws.subscribe('updateNodeMirror', handleMessage);
ws.unsubscribe('updateLinuxMirror', handleMessage);
};
}, []);
useEffect(() => {
getSystemConfig();
}, []);
return (
<div className="dependence-config-wrapper">
<Form layout="vertical" form={form} style={{ flexShrink: 0 }}>
<Form.Item
label={intl.get('代理')}
name="proxy"
extra={intl.get('代理与镜像源二选一即可')}
tooltip={{
title: intl.get('代理地址, 支持HTTP(S)/SOCK5'),
placement: 'topLeft',
}}
>
<Input.Group compact>
<Input
placeholder={'http://1.1.1.1:8080'}
style={{ width: 250 }}
value={systemConfig?.dependenceProxy}
onChange={(e) => {
setSystemConfig({
...systemConfig,
dependenceProxy: e.target.value,
});
}}
/>
<Button
type="primary"
loading={loading}
onClick={() => {
updateSystemConfig('dependence-proxy');
}}
style={{ width: 100 }}
>
{intl.get('确认')}
</Button>
</Input.Group>
</Form.Item>
<Form.Item
label={intl.get('Node 软件包镜像源')}
name="node"
tooltip={intl.get('NPM 镜像源')}
>
<Input.Group compact>
<Input
style={{ width: 250 }}
placeholder={'https://registry.npmmirror.com'}
value={systemConfig?.nodeMirror}
onChange={(e) => {
setSystemConfig({
...systemConfig,
nodeMirror: e.target.value,
});
}}
/>
<Button
type="primary"
loading={loading}
onClick={() => {
updateSystemConfigStream('node-mirror');
}}
style={{ width: 100 }}
>
{intl.get('确认')}
</Button>
</Input.Group>
</Form.Item>
<Form.Item
label={intl.get('Python 软件包镜像源')}
name="python"
tooltip={intl.get('PyPI 镜像源')}
>
<Input.Group compact>
<Input
style={{ width: 250 }}
placeholder={'https://pypi.doubanio.com/simple/'}
value={systemConfig?.pythonMirror}
onChange={(e) => {
setSystemConfig({
...systemConfig,
pythonMirror: e.target.value,
});
}}
/>
<Button
type="primary"
loading={loading}
onClick={() => {
updateSystemConfig('python-mirror');
}}
style={{ width: 100 }}
>
{intl.get('确认')}
</Button>
</Input.Group>
</Form.Item>
<Form.Item
label={intl.get('Linux 软件包镜像源')}
name="linux"
tooltip={intl.get('alpine linux 镜像源')}
>
<Input.Group compact>
<Input
style={{ width: 250 }}
placeholder={'https://mirrors.aliyun.com'}
value={systemConfig?.linuxMirror}
onChange={(e) => {
setSystemConfig({
...systemConfig,
linuxMirror: e.target.value,
});
}}
/>
<Button
type="primary"
loading={loading}
onClick={() => {
updateSystemConfigStream('linux-mirror');
}}
style={{ width: 100 }}
>
{intl.get('确认')}
</Button>
</Input.Group>
</Form.Item>
</Form>
<pre
style={{
fontFamily: 'Source Code Pro',
zoom: 0.83,
maxHeight: '100%',
overflowY: 'auto',
}}
>
<Ansi>{log}</Ansi>
</pre>
</div>
);
};
export default Dependence;
+5
View File
@@ -27,3 +27,8 @@
}
}
}
.dependence-config-wrapper {
display: flex;
gap: 40px;
}
+22 -9
View File
@@ -34,6 +34,7 @@ import { SharedContext } from '@/layouts';
import './index.less';
import useResizeObserver from '@react-hook/resize-observer';
import SystemLog from './systemLog';
import Dependence from './dependence';
const { Text } = Typography;
const isDemoEnv = window.__ENV__DeployEnv === 'demo';
@@ -74,11 +75,19 @@ const Setting = () => {
title: intl.get('权限'),
dataIndex: 'scopes',
key: 'scopes',
width: '40%',
width: 500,
render: (text: string, record: any) => {
return record.scopes.map((scope: any) => {
return <Tag key={scope}>{(config.scopesMap as any)[scope]}</Tag>;
});
return (
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{record.scopes.map((scope: any) => {
return (
<Tag style={{ marginRight: 0 }} key={scope}>
{(config.scopesMap as any)[scope]}
</Tag>
);
})}
</div>
);
},
},
{
@@ -347,21 +356,25 @@ const Setting = () => {
{
key: 'syslog',
label: intl.get('系统日志'),
children: <SystemLog data={systemLogData} height={height} theme={theme}/>,
children: (
<SystemLog data={systemLogData} height={height} theme={theme} />
),
},
{
key: 'login',
label: intl.get('登录日志'),
children: <LoginLog data={loginLogData} />,
},
{
key: 'dependence',
label: intl.get('依赖设置'),
children: <Dependence />,
},
{
key: 'other',
label: intl.get('其他设置'),
children: (
<Other
reloadTheme={reloadTheme}
systemInfo={systemInfo}
/>
<Other reloadTheme={reloadTheme} systemInfo={systemInfo} />
),
},
{
+23 -6
View File
@@ -21,6 +21,12 @@ import './index.less';
import { UploadOutlined } from '@ant-design/icons';
import Countdown from 'antd/lib/statistic/Countdown';
import useProgress from './progress';
import pick from 'lodash/pick';
const dataMap = {
'log-remove-frequency': 'logRemoveFrequency',
'cron-concurrency': 'cronConcurrency',
};
const Other = ({
systemInfo,
@@ -32,7 +38,6 @@ const Other = ({
cronConcurrency?: number | null;
}>();
const [form] = Form.useForm();
const modalRef = useRef<any>();
const [exportLoading, setExportLoading] = useState(false);
const showUploadProgress = useProgress(intl.get('上传'));
const showDownloadProgress = useProgress(intl.get('下载'));
@@ -80,9 +85,12 @@ const Other = ({
});
};
const updateSystemConfig = () => {
const updateSystemConfig = (path: keyof typeof dataMap) => {
request
.put(`${config.apiPrefix}system/config`, systemConfig)
.put(
`${config.apiPrefix}system/config/${path}`,
pick(systemConfig, dataMap[path]),
)
.then(({ code, data }) => {
if (code === 200) {
message.success(intl.get('更新成功'));
@@ -124,7 +132,12 @@ const Other = ({
maskClosable: false,
title: intl.get('确认重启'),
centered: true,
content: intl.get('备份数据上传成功,确认覆盖数据'),
content: (
<>
<div>{intl.get('备份数据上传成功,确认覆盖数据')}</div>
<div>{intl.get('如果恢复失败,可进入容器执行')} ql reload data</div>
</>
),
okText: intl.get('重启'),
onOk() {
request
@@ -207,7 +220,9 @@ const Other = ({
/>
<Button
type="primary"
onClick={updateSystemConfig}
onClick={() => {
updateSystemConfig('log-remove-frequency');
}}
style={{ width: 84 }}
>
{intl.get('确认')}
@@ -226,7 +241,9 @@ const Other = ({
/>
<Button
type="primary"
onClick={updateSystemConfig}
onClick={() => {
updateSystemConfig('cron-concurrency');
}}
style={{ width: 84 }}
>
{intl.get('确认')}
+4 -2
View File
@@ -3,6 +3,7 @@ const baseUrl = window.__ENV__QlBaseUrl || '/';
export default {
siteName: intl.get('青龙'),
baseUrl,
apiPrefix: `${baseUrl}api/`,
authKey: 'token',
@@ -320,12 +321,12 @@ export default {
{
label: 'emailService',
tip: intl.get(
'邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://nodemailer.com/smtp/well-known/',
'邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json',
),
required: true,
},
{ label: 'emailUser', tip: intl.get('邮箱地址'), required: true },
{ label: 'emailPass', tip: intl.get('邮箱SMTP授权码'), required: true },
{ label: 'emailPass', tip: intl.get('SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定'), required: true },
],
pushMe: [
{
@@ -369,6 +370,7 @@ export default {
tip: intl.get('请求头Content-Type'),
required: true,
items: [
{ value: 'text/plain' },
{ value: 'application/json' },
{ value: 'multipart/form-data' },
{ value: 'application/x-www-form-urlencoded' },
+4 -4
View File
@@ -1,13 +1,13 @@
import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';
import { loader } from '@monaco-editor/react';
import config from './config';
export function init(version: string) {
// sentry监控 init
Sentry.init({
dsn: 'https://3406424fb1dc4813a62d39e844a9d0ac@o1098464.ingest.sentry.io/6122818',
dsn: 'https://49b9ad1a6201bfe027db296ab7c6d672@o1098464.ingest.sentry.io/6122818',
integrations: [
new Integrations.BrowserTracing({
new Sentry.BrowserTracing({
shouldCreateSpanForRequest(url) {
return !url.includes('/api/ws') && !url.includes('/api/static');
},
@@ -27,7 +27,7 @@ export function init(version: string) {
// monaco 编辑器配置cdn和locale
loader.config({
paths: {
vs: 'https://cdn.staticfile.org/monaco-editor/0.33.0/min/vs',
vs: `${config.baseUrl}monaco-editor/min/vs`,
},
'vs/nls': {
availableLanguages: {
+3 -1
View File
@@ -5,4 +5,6 @@ export type SockMessageType =
| 'updateSystemVersion'
| 'manuallyRunScript'
| 'runSubscriptionEnd'
| 'reloadSystem';
| 'reloadSystem'
| 'updateNodeMirror'
| 'updateLinuxMirror';
+6 -17
View File
@@ -1,18 +1,7 @@
version: 2.16.4
changeLogLink: https://t.me/jiao_long/395
publishTime: 2023-10-18 23:00
version: 2.17.2
changeLogLink: https://t.me/jiao_long/403
publishTime: 2024-03-02 17:00
changeLog: |
1. 脚本推送增加自定义 webhook 方式
3. 增加 chronocat 无头模式的QQNT推送
2. task 命令支持给脚本传参,使用 -- 分割,后面的参数都会传给脚本
4. 系统日志增加置顶置底按钮
5. 定时任务详情增加额外定时展示
6. 定时任务增加前后规则校验,不能包含 task 命令
7. 修复定时任务间隔较小,任务状态不准确
8. 修复定时任务日志滚动
9. 修复 QlBaseUrl 末尾不加斜杠无法访问
10. 修复定时任务详情页查看历史日志异常
11. 修复定时任务添加 task_after 命令无法停止
12. 修复清空文件夹报错
13. 修复设置代理时, schedule 服务通信异常
14. 其他优化
1. 依赖管理支持队列中依赖取消安装,支持状态筛选
2. 修复 webhook 通知 body 拆分逻辑
3. 企业微信有长度限制,超长的进行分段提交 https://github.com/pharaoh2012