Compare commits

..
1 Commits
Author SHA1 Message Date
whyour c083d2f4d1 更新版本 v2.17.0 2024-01-18 13:51:17 +08:00
69 changed files with 1351 additions and 2400 deletions
-1
View File
@@ -1,4 +1,3 @@
UPDATE_PORT=5300
PUBLIC_PORT=5400 PUBLIC_PORT=5400
CRON_PORT=5500 CRON_PORT=5500
BACK_PORT=5600 BACK_PORT=5600
+25 -86
View File
@@ -3,50 +3,55 @@ name: Build And Push Docker Image
on: on:
push: push:
paths-ignore: paths-ignore:
- "*.md" - '.github/**'
- '*.md'
branches: branches:
- "master" - 'master'
- "develop" - 'develop'
tags: tags:
- "v*" - 'v*'
schedule: schedule:
- cron: "00 20 * * *" - cron: '00 20 * * *'
workflow_dispatch: workflow_dispatch:
jobs: jobs:
to_gitlab: to_gitlab:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1 - uses: pixta-dev/repository-mirroring-action@v1
with: with:
target_repo_url: git@gitlab.com:whyour/qinglong.git target_repo_url:
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }} git@gitlab.com:whyour/qinglong.git
ssh_private_key:
${{ secrets.GITLAB_SSH_PK }}
to_gitee: to_gitee:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1 - uses: pixta-dev/repository-mirroring-action@v1
with: with:
target_repo_url: git@gitee.com:whyour/qinglong.git target_repo_url:
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }} git@gitee.com:whyour/qinglong.git
ssh_private_key:
${{ secrets.GITLAB_SSH_PK }}
build-static: build-static:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v2
with: with:
version: "8.3.1" version: '8.3.1'
- uses: actions/setup-node@v4 - uses: actions/setup-node@v3
with: with:
cache: "pnpm" cache: 'pnpm'
- name: build front and back - name: build front and back
run: | run: |
@@ -78,7 +83,6 @@ jobs:
export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l git" export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l git"
git remote add gitee "${REPO_GITEE}" git remote add gitee "${REPO_GITEE}"
git remote add gitlab "${REPO_GITLAB}" git remote add gitlab "${REPO_GITLAB}"
git gc
git push --force --quiet gitee ${GITHUB_BRANCH}:${GITHUB_BRANCH} git push --force --quiet gitee ${GITHUB_BRANCH}:${GITHUB_BRANCH}
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH} git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH}
@@ -95,12 +99,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v2
with: with:
version: "8.3.1" version: '8.3.1'
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
cache: "pnpm" cache: 'pnpm'
- name: Setup timezone - name: Setup timezone
uses: szenius/set-timezone@v1.2 uses: szenius/set-timezone@v1.2
@@ -167,69 +171,4 @@ jobs:
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
- name: Image digest - name: Image digest
run: | run: echo ${{ steps.docker_build.outputs.digest }}
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 }}
-5
View File
@@ -16,11 +16,6 @@ export default defineConfig({
favicons: [`https://qn.whyour.cn/favicon.svg`], favicons: [`https://qn.whyour.cn/favicon.svg`],
publicPath: process.env.NODE_ENV === 'production' ? './' : '/', publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
proxy: { proxy: {
[`${baseUrl}api/update`]: {
target: 'http://127.0.0.1:5300/',
changeOrigin: true,
pathRewrite: { [`^${baseUrl}api/update`]: '/api' },
},
[`${baseUrl}api/public`]: { [`${baseUrl}api/public`]: {
target: 'http://127.0.0.1:5400/', target: 'http://127.0.0.1:5400/',
changeOrigin: true, changeOrigin: true,
+1 -9
View File
@@ -166,14 +166,6 @@ 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 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 ```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 npm install -g @whyour/qinglong
qinglong qinglong
# Add the environment variables QL_DIR and QL_DATA_DIR when prompted # Add the environment variables QL_DIR and QL_DATA_DIR when prompted
@@ -186,7 +178,7 @@ qinglong
## Development ## Development
```bash ```bash
$ git clone https://github.com/whyour/qinglong.git $ git clone git@github.com:whyour/qinglong.git
$ cd qinglong $ cd qinglong
$ cp .env.example .env $ cp .env.example .env
# Recommended use pnpm https://pnpm.io/zh/installation # Recommended use pnpm https://pnpm.io/zh/installation
+1 -9
View File
@@ -166,14 +166,6 @@ podman run -dit \
建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3 建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3
```bash ```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 npm install -g @whyour/qinglong
qinglong qinglong
# 根据提示增加环境变量 QL_DIR 和 QL_DATA_DIR # 根据提示增加环境变量 QL_DIR 和 QL_DATA_DIR
@@ -186,7 +178,7 @@ qinglong
## 开发 ## 开发
```bash ```bash
$ git clone https://github.com/whyour/qinglong.git $ git clone git@github.com:whyour/qinglong.git
$ cd qinglong $ cd qinglong
$ cp .env.example .env $ cp .env.example .env
# 推荐使用 pnpm https://pnpm.io/zh/installation # 推荐使用 pnpm https://pnpm.io/zh/installation
+19 -19
View File
@@ -7,7 +7,7 @@ import * as fs from 'fs/promises';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import { join } from 'path'; import { join } from 'path';
import { SAMPLE_FILES } from '../config/const'; import { SAMPLE_FILES } from '../config/const';
import ConfigService from '../services/config'; import got from 'got';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
@@ -50,9 +50,24 @@ export default (app: Router) => {
route.get( route.get(
'/detail', '/detail',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
const configService = Container.get(ConfigService); let content = '';
await configService.getFile(req.query.path as string, res); const _path = req.query.path as string;
if (config.blackFileList.includes(_path) || !_path) {
res.send({ code: 403, message: '文件无法访问' });
}
if (_path.startsWith('sample/')) {
const res = await got.get(
`https://gitlab.com/whyour/qinglong/-/raw/master/${_path}`,
);
content = res.body;
} else if (_path.startsWith('data/scripts/')) {
content = await getFileContentByName(join(config.rootPath, _path));
} else {
content = await getFileContentByName(join(config.configPath, _path));
}
res.send({ code: 200, data: content });
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
@@ -74,10 +89,7 @@ export default (app: Router) => {
if (config.blackFileList.includes(name)) { if (config.blackFileList.includes(name)) {
res.send({ code: 403, message: '文件无法访问' }); res.send({ code: 403, message: '文件无法访问' });
} }
let path = join(config.configPath, name); const path = join(config.configPath, name);
if (name.startsWith('data/scripts/')) {
path = join(config.rootPath, name);
}
await fs.writeFile(path, content); await fs.writeFile(path, content);
res.send({ code: 200, message: '保存成功' }); res.send({ code: 200, message: '保存成功' });
} catch (e) { } catch (e) {
@@ -85,16 +97,4 @@ export default (app: Router) => {
} }
}, },
); );
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,20 +134,4 @@ 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);
}
},
);
}; };
+1 -20
View File
@@ -26,29 +26,10 @@ 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( route.get(
'/:file', '/:file',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
if (blacklist.includes(req.path)) { if (blacklist.includes(req.path)) {
return res.send({ code: 403, message: '暂无权限' }); return res.send({ code: 403, message: '暂无权限' });
+5 -19
View File
@@ -72,31 +72,17 @@ 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( route.get(
'/:file', '/:file',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
const scriptService = Container.get(ScriptService); const filePath = join(
const content = await scriptService.getFile( config.scriptPath,
req.query.path as string, req.query.path as string,
req.params.file, req.params.file,
); );
const content = await getFileContentByName(filePath);
res.send({ code: 200, data: content }); res.send({ code: 200, data: content });
} catch (e) { } catch (e) {
return next(e); return next(e);
@@ -136,7 +122,7 @@ export default (app: Router) => {
} }
if (req.file) { if (req.file) {
await fs.rename(req.file.path, join(path, filename)); await fs.rename(req.file.path, join(path, req.file.filename));
return res.send({ code: 200 }); return res.send({ code: 200 });
} }
+1 -1
View File
@@ -216,7 +216,7 @@ export default (app: Router) => {
'/reload', '/reload',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
type: Joi.string().optional().allow('').allow(null), type: Joi.string().required(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
-3
View File
@@ -74,9 +74,6 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
if (process.env.DeployEnv === 'demo') {
return res.send({ code: 450, message: '未知错误' });
}
const userService = Container.get(UserService); const userService = Container.get(UserService);
await userService.updateUsernameAndPassword(req.body); await userService.updateUsernameAndPassword(req.body);
res.send({ code: 200, message: '更新成功' }); res.send({ code: 200, message: '更新成功' });
+1 -1
View File
@@ -15,7 +15,7 @@ async function startServer() {
await require('./loaders/app').default({ expressApp: app }); await require('./loaders/app').default({ expressApp: app });
const server = app const server = app
.listen(config.port, '0.0.0.0', () => { .listen(config.port, () => {
Logger.debug(`✌️ 后端服务启动成功!`); Logger.debug(`✌️ 后端服务启动成功!`);
console.debug(`✌️ 后端服务启动成功!`); console.debug(`✌️ 后端服务启动成功!`);
process.send?.('ready'); process.send?.('ready');
-1
View File
@@ -57,7 +57,6 @@ export default {
port: parseInt(process.env.BACK_PORT as string, 10), port: parseInt(process.env.BACK_PORT as string, 10),
cronPort: parseInt(process.env.CRON_PORT as string, 10), cronPort: parseInt(process.env.CRON_PORT as string, 10),
publicPort: parseInt(process.env.PUBLIC_PORT as string, 10), publicPort: parseInt(process.env.PUBLIC_PORT as string, 10),
updatePort: parseInt(process.env.UPDATE_PORT as string, 10),
secret: process.env.SECRET || createRandomString(16, 32), secret: process.env.SECRET || createRandomString(16, 32),
logs: { logs: {
level: process.env.LOG_LEVEL || 'silly', level: process.env.LOG_LEVEL || 'silly',
+28 -37
View File
@@ -30,7 +30,7 @@ export async function getLastModifyFilePath(dir: string) {
arr.forEach(async (item) => { arr.forEach(async (item) => {
const fullpath = path.join(dir, item); const fullpath = path.join(dir, item);
const stats = await fs.lstat(fullpath); const stats = await fs.stat(fullpath);
if (stats.isFile()) { if (stats.isFile()) {
if (stats.mtimeMs >= 0) { if (stats.mtimeMs >= 0) {
filePath = fullpath; filePath = fullpath;
@@ -257,7 +257,7 @@ export async function readDirs(
for (const file of files) { for (const file of files) {
const subPath = path.join(dir, file); const subPath = path.join(dir, file);
const stats = await fs.lstat(subPath); const stats = await fs.stat(subPath);
const key = path.join(relativePath, file); const key = path.join(relativePath, file);
if (blacklist.includes(file) || stats.isSymbolicLink()) { if (blacklist.includes(file) || stats.isSymbolicLink()) {
@@ -300,7 +300,7 @@ export async function readDir(
.filter((x) => !blacklist.includes(x)) .filter((x) => !blacklist.includes(x))
.map(async (file: string) => { .map(async (file: string) => {
const subPath = path.join(dir, file); const subPath = path.join(dir, file);
const stats = await fs.lstat(subPath); const stats = await fs.stat(subPath);
const key = path.join(relativePath, file); const key = path.join(relativePath, file);
return { return {
title: file, title: file,
@@ -360,35 +360,6 @@ export function parseHeaders(headers: string) {
return parsed; return parsed;
} }
function parseString(
input: string,
valueFormatFn?: (v: string) => string,
): Record<string, string> {
const regex = /(\w+):\s*((?:(?!\n\w+:).)*)/g;
const matches: Record<string, string> = {};
let match;
while ((match = regex.exec(input)) !== null) {
const [, key, value] = match;
const _key = key.trim();
if (!_key || matches[_key]) {
continue;
}
let _value = value.trim();
try {
_value = valueFormatFn ? valueFormatFn(_value) : _value;
const jsonValue = JSON.parse(_value);
matches[_key] = jsonValue;
} catch (error) {
matches[_key] = _value;
}
}
return matches;
}
export function parseBody( export function parseBody(
body: string, body: string,
contentType: contentType:
@@ -396,13 +367,33 @@ export function parseBody(
| 'multipart/form-data' | 'multipart/form-data'
| 'application/x-www-form-urlencoded' | 'application/x-www-form-urlencoded'
| 'text/plain', | 'text/plain',
valueFormatFn?: (v: string) => string,
) { ) {
if (contentType === 'text/plain' || !body) { if (contentType === 'text/plain' || !body) {
return valueFormatFn && body ? valueFormatFn(body) : body; return body;
} }
const parsed = parseString(body, valueFormatFn); 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();
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;
}
});
switch (contentType) { switch (contentType) {
case 'multipart/form-data': case 'multipart/form-data':
@@ -444,8 +435,8 @@ export async function killTask(pid: number) {
} }
} }
export async function getPid(cmd: string) { export async function getPid(name: string) {
const taskCommand = `ps -eo pid,command | grep "${cmd}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`; const taskCommand = `ps -eo pid,command | grep "${name}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`;
const pid = await promiseExec(taskCommand); const pid = await promiseExec(taskCommand);
return pid ? Number(pid) : undefined; return pid ? Number(pid) : undefined;
} }
-1
View File
@@ -32,7 +32,6 @@ export enum DependenceStatus {
'removed', 'removed',
'removeFailed', 'removeFailed',
'queued', 'queued',
'cancelled',
} }
export enum DependenceTypes { export enum DependenceTypes {
-10
View File
@@ -14,7 +14,6 @@ export enum NotificationMode {
'aibotk' = 'aibotk', 'aibotk' = 'aibotk',
'iGot' = 'iGot', 'iGot' = 'iGot',
'pushPlus' = 'pushPlus', 'pushPlus' = 'pushPlus',
'wePlusBot' = 'wePlusBot',
'email' = 'email', 'email' = 'email',
'pushMe' = 'pushMe', 'pushMe' = 'pushMe',
'feishu' = 'feishu', 'feishu' = 'feishu',
@@ -59,7 +58,6 @@ export class BarkNotification extends NotificationBaseInfo {
public barkGroup = 'qinglong'; public barkGroup = 'qinglong';
public barkLevel = 'active'; public barkLevel = 'active';
public barkUrl = ''; public barkUrl = '';
public barkArchive=""
} }
export class TelegramBotNotification extends NotificationBaseInfo { export class TelegramBotNotification extends NotificationBaseInfo {
@@ -101,12 +99,6 @@ export class PushPlusNotification extends NotificationBaseInfo {
public pushPlusUser = ''; public pushPlusUser = '';
} }
export class WePlusBotNotification extends NotificationBaseInfo {
public wePlusBotToken = '';
public wePlusBotReceiver = '';
public wePlusBotVersion = '';
}
export class EmailNotification extends NotificationBaseInfo { export class EmailNotification extends NotificationBaseInfo {
public emailService: string = ''; public emailService: string = '';
public emailUser: string = ''; public emailUser: string = '';
@@ -115,7 +107,6 @@ export class EmailNotification extends NotificationBaseInfo {
export class PushMeNotification extends NotificationBaseInfo { export class PushMeNotification extends NotificationBaseInfo {
public pushMeKey: string = ''; public pushMeKey: string = '';
public pushMeUrl: string = '';
} }
export class ChronocatNotification extends NotificationBaseInfo { export class ChronocatNotification extends NotificationBaseInfo {
@@ -153,7 +144,6 @@ export interface NotificationInfo
AibotkNotification, AibotkNotification,
IGotNotification, IGotNotification,
PushPlusNotification, PushPlusNotification,
WePlusBotNotification,
EmailNotification, EmailNotification,
PushMeNotification, PushMeNotification,
WebhookNotification, WebhookNotification,
+2 -5
View File
@@ -23,21 +23,18 @@ async function linkCommand() {
{ {
src: 'update.sh', src: 'update.sh',
dest: 'ql', dest: 'ql',
tmp: 'ql_tmp',
}, },
{ {
src: 'task.sh', src: 'task.sh',
dest: 'task', dest: 'task',
tmp: 'task_tmp',
}, },
]; ];
for (const link of linkShell) { for (const link of linkShell) {
const source = path.join(config.rootPath, 'shell', link.src); const source = path.join(config.rootPath, 'shell', link.src);
const target = path.join(commandDir, link.dest); const target = path.join(commandDir, link.dest);
const tmpTarget = path.join(commandDir, link.tmp); await rmPath(target);
await fs.symlink(source, tmpTarget); await fs.symlink(source, target);
await fs.rename(tmpTarget, target);
} }
} }
+16 -46
View File
@@ -10,57 +10,11 @@ import config from '../config';
import { CrontabViewModel, CronViewType } from '../data/cronView'; import { CrontabViewModel, CronViewType } from '../data/cronView';
import { initPosition } from '../data/env'; import { initPosition } from '../data/env';
import { AuthDataType, SystemModel } from '../data/system'; import { AuthDataType, SystemModel } from '../data/system';
import SystemService from '../services/system';
export default async () => { export default async () => {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const systemService = Container.get(SystemService);
const 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({ CrontabViewModel.findAll({
@@ -79,6 +33,22 @@ export default async () => {
// 初始化更新所有任务状态为空闲 // 初始化更新所有任务状态为空闲
await CrontabModel.update({ status: CrontabStatus.idle }, { where: {} }); 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 任务 // 初始化时执行一次所有的 ql repo 任务
CrontabModel.findAll({ CrontabModel.findAll({
where: { where: {
+22
View File
@@ -5,6 +5,28 @@ import Sock from './sock';
export default async ({ server }: { server: Server }) => { export default async ({ server }: { server: Server }) => {
await Sock({ server }); await Sock({ server });
Logger.info('✌️ Sock loaded'); Logger.info('✌️ Sock loaded');
let exitTime = 0;
let timer: NodeJS.Timeout;
process.on('SIGINT', (singal) => {
Logger.warn(`Server need close, singal ${singal}`);
console.warn(`Server need close, singal ${singal}`);
exitTime++;
if (exitTime >= 3) {
Logger.warn('Forcing server close');
console.warn('Forcing server close');
clearTimeout(timer);
process.exit(1);
}
server.close(() => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
process.exit();
}, 15000);
});
});
process.on('uncaughtException', (error) => { process.on('uncaughtException', (error) => {
Logger.error('Uncaught exception:', error); Logger.error('Uncaught exception:', error);
-106
View File
@@ -1,106 +0,0 @@
import bodyParser from 'body-parser';
import { errors } from 'celebrate';
import cors from 'cors';
import { Application, NextFunction, Request, Response } from 'express';
import jwt from 'express-jwt';
import Container from 'typedi';
import config from '../config';
import SystemService from '../services/system';
import Logger from './logger';
export default ({ app }: { app: Application }) => {
app.set('trust proxy', 'loopback');
app.use(cors());
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use(
jwt({
secret: config.secret,
algorithms: ['HS384'],
}),
);
app.put(
'/api/reload',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.reloadSystem();
res.send(result);
} catch (e) {
Logger.error('🔥 error: %o', e);
return next(e);
}
},
);
app.put(
'/api/system',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.reloadSystem('system');
res.send(result);
} catch (e) {
Logger.error('🔥 error: %o', e);
return next(e);
}
},
);
app.put(
'/api/data',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.reloadSystem('data');
res.send(result);
} catch (e) {
Logger.error('🔥 error: %o', e);
return next(e);
}
},
);
app.use((req, res, next) => {
const err: any = new Error('Not Found');
err['status'] = 404;
next(err);
});
app.use(errors());
app.use(
(
err: Error & { status: number },
req: Request,
res: Response,
next: NextFunction,
) => {
if (err.name === 'UnauthorizedError') {
return res
.status(err.status)
.send({ code: 401, message: err.message })
.end();
}
return next(err);
},
);
app.use(
(
err: Error & { status: number },
req: Request,
res: Response,
next: NextFunction,
) => {
res.status(err.status || 500);
res.json({
code: err.status || 500,
message: err.message,
});
},
);
};
+1 -1
View File
@@ -21,7 +21,7 @@ app.get('/api/health', (req, res) => {
}); });
app app
.listen(config.publicPort, '0.0.0.0', async () => { .listen(config.publicPort, async () => {
await require('./loaders/sentry').default({ expressApp: app }); await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default(); await require('./loaders/db').default();
-30
View File
@@ -1,30 +0,0 @@
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 });
}
}
+8 -15
View File
@@ -407,7 +407,7 @@ export default class CronService {
} }
private async runSingle(cronId: number): Promise<number | void> { private async runSingle(cronId: number): Promise<number | void> {
return taskLimit.manualRunWithCronLimit(() => { return taskLimit.runWithCronLimit(() => {
return new Promise(async (resolve: any) => { return new Promise(async (resolve: any) => {
const cron = await this.getDb({ id: cronId }); const cron = await this.getDb({ id: cronId });
const params = { const params = {
@@ -434,11 +434,9 @@ export default class CronService {
} }
const logPath = `${uniqPath}/${logTime}.log`; const logPath = `${uniqPath}/${logTime}.log`;
const absolutePath = path.resolve(config.logPath, `${logPath}`); const absolutePath = path.resolve(config.logPath, `${logPath}`);
const cp = spawn( const cp = spawn(
`real_log_path=${logPath} no_delay=true ${this.makeCommand( `real_log_path=${logPath} no_delay=true ${this.makeCommand(cron)}`,
cron,
true
)}`,
{ shell: '/bin/bash' }, { shell: '/bin/bash' },
); );
@@ -446,14 +444,11 @@ export default class CronService {
{ status: CrontabStatus.running, pid: cp.pid, log_path: logPath }, { status: CrontabStatus.running, pid: cp.pid, log_path: logPath },
{ where: { id } }, { where: { id } },
); );
cp.stdout.on('data', async (data) => {
await fs.appendFile(absolutePath, data.toString());
});
cp.stderr.on('data', async (data) => { cp.stderr.on('data', async (data) => {
await fs.appendFile(absolutePath, data.toString()); await fs.appendFile(`${absolutePath}`, `${data.toString()}`);
}); });
cp.on('error', async (err) => { cp.on('error', async (err) => {
await fs.appendFile(absolutePath, JSON.stringify(err)); await fs.appendFile(`${absolutePath}`, `${JSON.stringify(err)}`);
}); });
cp.on('exit', async (code) => { cp.on('exit', async (code) => {
@@ -520,7 +515,7 @@ export default class CronService {
files.map(async (x) => ({ files.map(async (x) => ({
filename: x, filename: x,
directory: relativeDir.replace(config.logPath, ''), directory: relativeDir.replace(config.logPath, ''),
time: (await fs.lstat(`${dir}/${x}`)).mtime.getTime(), time: (await fs.stat(`${dir}/${x}`)).mtime.getTime(),
})), })),
) )
).sort((a, b) => b.time - a.time); ).sort((a, b) => b.time - a.time);
@@ -529,14 +524,12 @@ export default class CronService {
} }
} }
private makeCommand(tab: Crontab, realTime?: boolean) { private makeCommand(tab: Crontab) {
let command = tab.command.trim(); let command = tab.command.trim();
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) { if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
command = `${TASK_PREFIX}${tab.command}`; command = `${TASK_PREFIX}${tab.command}`;
} }
let commandVariable = `real_time=${Boolean(realTime)} no_tee=true ID=${ let commandVariable = `no_tee=true ID=${tab.id} `;
tab.id
} `;
if (tab.task_before) { if (tab.task_before) {
commandVariable += `task_before='${tab.task_before commandVariable += `task_before='${tab.task_before
.replace(/'/g, "'\\''") .replace(/'/g, "'\\''")
+7 -60
View File
@@ -14,12 +14,7 @@ import {
import { spawn } from 'cross-spawn'; import { spawn } from 'cross-spawn';
import SockService from './sock'; import SockService from './sock';
import { FindOptions, Op } from 'sequelize'; import { FindOptions, Op } from 'sequelize';
import { import { fileExist, promiseExecSuccess } from '../config/util';
fileExist,
getPid,
killTask,
promiseExecSuccess,
} from '../config/util';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import taskLimit from '../shared/pLimit'; import taskLimit from '../shared/pLimit';
@@ -91,21 +86,11 @@ export default class DependenceService {
} }
public async dependencies( public async dependencies(
{ { searchValue, type }: { searchValue: string; type: string },
searchValue, sort: any = { position: -1 },
type,
status,
}: { searchValue: string; type: string; status: string },
sort: any = [],
query: any = {}, query: any = {},
): Promise<Dependence[]> { ): Promise<Dependence[]> {
let condition = { let condition = { ...query, type: DependenceTypes[type as any] };
...query,
type: DependenceTypes[type as any],
};
if (status) {
condition.status = status.split(',').map(Number);
}
if (searchValue) { if (searchValue) {
const encodeText = encodeURI(searchValue); const encodeText = encodeURI(searchValue);
const reg = { const reg = {
@@ -121,7 +106,7 @@ export default class DependenceService {
}; };
} }
try { try {
const result = await this.find(condition, sort); const result = await this.find(condition);
return result as any; return result as any;
} catch (error) { } catch (error) {
throw error; throw error;
@@ -149,28 +134,6 @@ export default class DependenceService {
return docs; 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[]> { private async find(query: any, sort: any = []): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({ const docs = await DependenceModel.findAll({
where: { ...query }, where: { ...query },
@@ -205,14 +168,8 @@ export default class DependenceService {
isInstall: boolean = true, isInstall: boolean = true,
force: boolean = false, force: boolean = false,
) { ) {
return taskLimit.runDependeny(dependency, () => { return taskLimit.runOneByOne(() => {
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
if (taskLimit.firstDependencyId !== dependency.id) {
return resolve(null);
}
taskLimit.removeQueuedDependency(dependency);
const depIds = [dependency.id!]; const depIds = [dependency.id!];
const status = isInstall const status = isInstall
? DependenceStatus.installing ? DependenceStatus.installing
@@ -360,17 +317,7 @@ export default class DependenceService {
? DependenceStatus.installFailed ? DependenceStatus.installFailed
: DependenceStatus.removeFailed; : DependenceStatus.removeFailed;
} }
const docs = await DependenceModel.findAll({ where: { id: depIds } }); await DependenceModel.update({ status }, { 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) { if ((isSucceed || force) && !isInstall) {
+29 -58
View File
@@ -27,7 +27,6 @@ export default class NotificationService {
['aibotk', this.aibotk], ['aibotk', this.aibotk],
['iGot', this.iGot], ['iGot', this.iGot],
['pushPlus', this.pushPlus], ['pushPlus', this.pushPlus],
['wePlusBot',this.wePlusBot],
['email', this.email], ['email', this.email],
['pushMe', this.pushMe], ['pushMe', this.pushMe],
['webhook', this.webhook], ['webhook', this.webhook],
@@ -197,15 +196,8 @@ export default class NotificationService {
} }
private async bark() { private async bark() {
let { let { barkPush, barkIcon, barkSound, barkGroup, barkLevel, barkUrl } =
barkPush, this.params;
barkIcon = '',
barkSound = '',
barkGroup = '',
barkLevel = '',
barkUrl = '',
barkArchive = '',
} = this.params;
if (!barkPush.startsWith('http')) { if (!barkPush.startsWith('http')) {
barkPush = `https://api.day.app/${barkPush}`; barkPush = `https://api.day.app/${barkPush}`;
} }
@@ -213,7 +205,8 @@ export default class NotificationService {
this.title, this.title,
)}/${encodeURIComponent( )}/${encodeURIComponent(
this.content, this.content,
)}?icon=${barkIcon}&sound=${barkSound}&group=${barkGroup}&level=${barkLevel}&url=${barkUrl}&isArchive=${barkArchive}`; )}?icon=${barkIcon}&sound=${barkSound}&group=${barkGroup}&level=${barkLevel}&url=${barkUrl}`;
try { try {
const res: any = await got const res: any = await got
.get(url, { .get(url, {
@@ -514,42 +507,6 @@ export default class NotificationService {
} }
} }
private async wePlusBot() {
const { wePlusBotToken, wePlusBotReceiver, wePlusBotVersion } = this.params;
let content = this.content;
let template = 'txt';
if(this.content.length>800){
template = 'html';
content = content.replace(/[\n\r]/g, '<br>');
}
const url = `https://www.weplusbot.com/send`;
try {
const res: any = await got
.post(url, {
...this.gotOption,
json: {
token: `${wePlusBotToken}`,
title: `${this.title}`,
template: `${template}`,
content: `${content}`,
receiver: `${wePlusBotReceiver || ''}`,
version: `${wePlusBotVersion || 'pro'}`,
},
})
.json();
if (res.code === 200) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async lark() { private async lark() {
let { larkKey } = this.params; let { larkKey } = this.params;
@@ -568,7 +525,7 @@ export default class NotificationService {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}) })
.json(); .json();
if (res.StatusCode === 0 || res.code === 0) { if (res.StatusCode === 0) {
return true; return true;
} else { } else {
throw new Error(JSON.stringify(res)); throw new Error(JSON.stringify(res));
@@ -610,14 +567,13 @@ export default class NotificationService {
} }
private async pushMe() { private async pushMe() {
const { pushMeKey, pushMeUrl } = this.params; const { pushMeKey } = this.params;
try { try {
const res: any = await got.post( const res: any = await got.post(
pushMeUrl || 'https://push.i-i.me/', `https://push.i-i.me/?push_key=${pushMeKey}`,
{ {
...this.gotOption, ...this.gotOption,
json: { json: {
push_key: pushMeKey,
title: this.title, title: this.title,
content: this.content, content: this.content,
}, },
@@ -700,14 +656,17 @@ export default class NotificationService {
webhookContentType, webhookContentType,
} = this.params; } = this.params;
if (!webhookUrl.includes('$title') && !webhookBody.includes('$title')) { const { formatBody, formatUrl } = this.formatNotifyContent(
webhookUrl,
webhookBody,
);
if (!formatUrl && !formatBody) {
throw new Error('Url 或者 Body 中必须包含 $title'); throw new Error('Url 或者 Body 中必须包含 $title');
} }
const headers = parseHeaders(webhookHeaders); const headers = parseHeaders(webhookHeaders);
const body = parseBody(webhookBody, webhookContentType, (v) => const body = parseBody(formatBody, webhookContentType);
v?.replaceAll('$title', this.title)?.replaceAll('$content', this.content),
);
const bodyParam = this.formatBody(webhookContentType, body); const bodyParam = this.formatBody(webhookContentType, body);
const options = { const options = {
method: webhookMethod, method: webhookMethod,
@@ -717,9 +676,6 @@ export default class NotificationService {
...bodyParam, ...bodyParam,
}; };
try { try {
const formatUrl = webhookUrl
?.replaceAll('$title', encodeURIComponent(this.title))
?.replaceAll('$content', encodeURIComponent(this.content));
const res = await got(formatUrl, options); const res = await got(formatUrl, options);
if (String(res.statusCode).startsWith('20')) { if (String(res.statusCode).startsWith('20')) {
return true; return true;
@@ -744,4 +700,19 @@ export default class NotificationService {
} }
return {}; return {};
} }
private formatNotifyContent(url: string, body: string) {
if (!url.includes('$title') && !body.includes('$title')) {
return {};
}
return {
formatUrl: url
?.replaceAll('$title', encodeURIComponent(this.title))
?.replaceAll('$content', encodeURIComponent(this.content)),
formatBody: body
?.replaceAll('$title', this.title)
?.replaceAll('$content', this.content),
};
}
} }
+1 -1
View File
@@ -13,7 +13,7 @@ import taskLimit from '../shared/pLimit';
import { spawn } from 'cross-spawn'; import { spawn } from 'cross-spawn';
export interface ScheduleTaskType { export interface ScheduleTaskType {
id?: number; id: number;
command: string; command: string;
name?: string; name?: string;
schedule?: string; schedule?: string;
+3 -12
View File
@@ -1,12 +1,12 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import path, { join } from 'path'; import path from 'path';
import SockService from './sock'; import SockService from './sock';
import CronService from './cron'; import CronService from './cron';
import ScheduleService, { TaskCallbacks } from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
import config from '../config'; import config from '../config';
import { TASK_COMMAND } from '../config/const'; import { TASK_COMMAND } from '../config/const';
import { getFileContentByName, getPid, killTask, rmPath } from '../config/util'; import { getPid, killTask, rmPath } from '../config/util';
@Service() @Service()
export default class ScriptService { export default class ScriptService {
@@ -41,7 +41,7 @@ export default class ScriptService {
const relativePath = path.relative(config.scriptPath, filePath); const relativePath = path.relative(config.scriptPath, filePath);
const command = `${TASK_COMMAND} ${relativePath} now`; const command = `${TASK_COMMAND} ${relativePath} now`;
const pid = await this.scheduleService.runTask( const pid = await this.scheduleService.runTask(
`real_time=true ${command}`, command,
this.taskCallbacks(filePath), this.taskCallbacks(filePath),
{ command }, { command },
'start', 'start',
@@ -61,13 +61,4 @@ export default class ScriptService {
return { code: 200 }; return { code: 200 };
} }
public async getFile(filePath: string, fileName: string) {
let _filePath = join(config.scriptPath, filePath, fileName);
if (filePath.startsWith(config.dataPath)) {
_filePath = join(filePath, fileName);
}
const content = await getFileContentByName(_filePath);
return content;
}
} }
+6 -9
View File
@@ -91,9 +91,9 @@ export default class SubscriptionService {
this.taskCallbacks(doc), this.taskCallbacks(doc),
runImmediately, runImmediately,
)); ));
} else if (doc.interval_schedule) { } else {
this.scheduleService.cancelIntervalTask(doc as any); this.scheduleService.cancelIntervalTask(doc as any);
const { type, value } = doc.interval_schedule; const { type, value } = doc.interval_schedule as any;
needCreate && needCreate &&
(await this.scheduleService.createIntervalTask( (await this.scheduleService.createIntervalTask(
doc as any, doc as any,
@@ -256,7 +256,7 @@ export default class SubscriptionService {
); );
} }
public async remove(ids: number[], query: { force?: boolean }) { public async remove(ids: number[], query: any) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) { for (const doc of docs) {
await this.handleTask(doc, false); await this.handleTask(doc, false);
@@ -279,11 +279,8 @@ export default class SubscriptionService {
public async getDb( public async getDb(
query: FindOptions<Subscription>['where'], query: FindOptions<Subscription>['where'],
): Promise<Subscription> { ): Promise<Subscription> {
const doc = await SubscriptionModel.findOne({ where: { ...query } }); const doc: any = await SubscriptionModel.findOne({ where: { ...query } });
if (!doc) { return doc && (doc.get({ plain: true }) as Subscription);
throw new Error(`${JSON.stringify(query)} not found`);
}
return doc.get({ plain: true });
} }
public async run(ids: number[]) { public async run(ids: number[]) {
@@ -374,7 +371,7 @@ export default class SubscriptionService {
files.map(async (x) => ({ files.map(async (x) => ({
filename: x, filename: x,
directory: relativeDir.replace(config.logPath, ''), directory: relativeDir.replace(config.logPath, ''),
time: (await fs.lstat(`${dir}/${x}`)).mtime.getTime(), time: (await fs.stat(`${dir}/${x}`)).mtime.getTime(),
})), })),
) )
).sort((a, b) => b.time - a.time); ).sort((a, b) => b.time - a.time);
+66 -61
View File
@@ -1,13 +1,20 @@
import { spawn } from 'cross-spawn';
import { Response } from 'express'; import { Response } from 'express';
import fs from 'fs'; import { Service, Inject } from 'typedi';
import got from 'got';
import sum from 'lodash/sum';
import path from 'path';
import { Inject, Service } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import config from '../config'; import config from '../config';
import { TASK_COMMAND } from '../config/const'; import {
AuthDataType,
AuthInfo,
SystemInstance,
SystemModel,
SystemModelInfo,
} from '../data/system';
import { NotificationInfo } from '../data/notify';
import NotificationService from './notify';
import ScheduleService, { TaskCallbacks } from './schedule';
import { spawn } from 'cross-spawn';
import SockService from './sock';
import got from 'got';
import { import {
getPid, getPid,
killTask, killTask,
@@ -16,23 +23,12 @@ import {
promiseExec, promiseExec,
readDirs, readDirs,
} from '../config/util'; } from '../config/util';
import { import { TASK_COMMAND } from '../config/const';
DependenceModel,
DependenceStatus,
DependenceTypes,
} from '../data/dependence';
import { NotificationInfo } from '../data/notify';
import {
AuthDataType,
AuthInfo,
SystemInstance,
SystemModel,
SystemModelInfo,
} from '../data/system';
import taskLimit from '../shared/pLimit'; import taskLimit from '../shared/pLimit';
import NotificationService from './notify'; import tar from 'tar';
import ScheduleService, { TaskCallbacks } from './schedule'; import path from 'path';
import SockService from './sock'; import fs from 'fs';
import sum from 'lodash/sum';
@Service() @Service()
export default class SystemService { export default class SystemService {
@@ -130,7 +126,7 @@ export default class SystemService {
return { code: 200, data: info }; return { code: 200, data: info };
} }
public async updateNodeMirror(info: SystemModelInfo, res?: Response) { public async updateNodeMirror(info: SystemModelInfo, res: Response) {
const oDoc = await this.getSystemConfig(); const oDoc = await this.getSystemConfig();
await this.updateAuthDb({ await this.updateAuthDb({
...oDoc, ...oDoc,
@@ -140,22 +136,13 @@ export default class SystemService {
if (info.nodeMirror) { if (info.nodeMirror) {
cmd = `pnpm config set registry ${info.nodeMirror}`; cmd = `pnpm config set registry ${info.nodeMirror}`;
} }
let command = `cd && ${cmd}`; const command = `cd && ${cmd} && pnpm i -g`;
const docs = await DependenceModel.findAll({
where: {
type: DependenceTypes.nodejs,
status: DependenceStatus.installed,
},
});
if (docs.length > 0) {
command += ` && pnpm i -g`;
}
this.scheduleService.runTask( this.scheduleService.runTask(
command, command,
{ {
onStart: async (cp) => { onStart: async (cp) => {
res?.setHeader('QL-Task-Pid', `${cp.pid}`); res.setHeader('QL-Task-Pid', `${cp.pid}`);
res?.end(); res.end();
}, },
onEnd: async () => { onEnd: async () => {
this.sockService.sendMessage({ this.sockService.sendMessage({
@@ -190,11 +177,7 @@ export default class SystemService {
return { code: 200, data: info }; return { code: 200, data: info };
} }
public async updateLinuxMirror( public async updateLinuxMirror(info: SystemModelInfo, res: Response) {
info: SystemModelInfo,
res?: Response,
onEnd?: () => void,
) {
const oDoc = await this.getSystemConfig(); const oDoc = await this.getSystemConfig();
await this.updateAuthDb({ await this.updateAuthDb({
...oDoc, ...oDoc,
@@ -202,12 +185,8 @@ export default class SystemService {
}); });
let defaultDomain = 'https://dl-cdn.alpinelinux.org'; let defaultDomain = 'https://dl-cdn.alpinelinux.org';
let targetDomain = 'https://dl-cdn.alpinelinux.org'; let targetDomain = 'https://dl-cdn.alpinelinux.org';
const content = await fs.promises.readFile('/etc/apk/repositories', { if (oDoc.info?.linuxMirror) {
encoding: 'utf-8', defaultDomain = oDoc.info.linuxMirror;
});
const domainMatch = content.match(/(http.*)\/alpine\/.*/);
if (domainMatch) {
defaultDomain = domainMatch[1];
} }
if (info.linuxMirror) { if (info.linuxMirror) {
targetDomain = info.linuxMirror; targetDomain = info.linuxMirror;
@@ -224,15 +203,14 @@ export default class SystemService {
command, command,
{ {
onStart: async (cp) => { onStart: async (cp) => {
res?.setHeader('QL-Task-Pid', `${cp.pid}`); res.setHeader('QL-Task-Pid', `${cp.pid}`);
res?.end(); res.end();
}, },
onEnd: async () => { onEnd: async () => {
this.sockService.sendMessage({ this.sockService.sendMessage({
type: 'updateLinuxMirror', type: 'updateLinuxMirror',
message: 'update linux mirror end', message: 'update linux mirror end',
}); });
onEnd?.();
}, },
onError: async (message: string) => { onError: async (message: string) => {
this.sockService.sendMessage({ type: 'updateLinuxMirror', message }); this.sockService.sendMessage({ type: 'updateLinuxMirror', message });
@@ -332,10 +310,31 @@ export default class SystemService {
return { code: 200 }; return { code: 200 };
} }
public async reloadSystem(target?: 'system' | 'data') { public async reloadSystem(target: 'system' | 'data') {
const cmd = `real_time=true ql reload ${target || ''}`; const cmd = `real_time=true ql reload ${target || ''}`;
const cp = spawn(cmd, { shell: '/bin/bash' }); const cp = spawn(cmd, { shell: '/bin/bash' });
cp.unref();
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'reloadSystem',
message: data.toString(),
});
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'reloadSystem',
message: data.toString(),
});
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'reloadSystem',
message: JSON.stringify(err),
});
});
return { code: 200 }; return { code: 200 };
} }
@@ -348,13 +347,20 @@ export default class SystemService {
} }
} }
public async run({ command }: { command: string }, callback: TaskCallbacks) { public async run(
{ command, logPath }: { command: string; logPath: string },
callback: TaskCallbacks,
) {
if (!command.startsWith(TASK_COMMAND)) { if (!command.startsWith(TASK_COMMAND)) {
command = `${TASK_COMMAND} ${command}`; command = `${TASK_COMMAND} ${command}`;
} }
this.scheduleService.runTask(`real_time=true ${command}`, callback, { this.scheduleService.runTask(
`real_log_path=${logPath} real_time=true ${command}`,
callback,
{
command, command,
}); },
);
} }
public async stop({ command, pid }: { command: string; pid: number }) { public async stop({ command, pid }: { command: string; pid: number }) {
@@ -381,8 +387,9 @@ export default class SystemService {
public async exportData(res: Response) { public async exportData(res: Response) {
try { try {
await promiseExec( await tar.create(
`cd ${config.rootPath} && tar -zcvf ${config.dataTgzFile} data/`, { gzip: true, file: config.dataTgzFile, cwd: config.rootPath },
['data'],
); );
res.download(config.dataTgzFile); res.download(config.dataTgzFile);
} catch (error: any) { } catch (error: any) {
@@ -393,10 +400,8 @@ export default class SystemService {
public async importData() { public async importData() {
try { try {
await promiseExec(`rm -rf ${path.join(config.tmpPath, 'data')}`); await promiseExec(`rm -rf ${path.join(config.tmpPath, 'data')}`);
const res = await promiseExec( await tar.x({ file: config.dataTgzFile, cwd: config.tmpPath });
`cd ${config.tmpPath} && tar -zxvf data.tgz`, return { code: 200 };
);
return { code: 200, data: res };
} catch (error: any) { } catch (error: any) {
return { code: 400, message: error.message }; return { code: 400, message: error.message };
} }
+20 -58
View File
@@ -2,22 +2,11 @@ import PQueue, { QueueAddOptions } from 'p-queue-cjs';
import os from 'os'; import os from 'os';
import { AuthDataType, SystemModel } from '../data/system'; import { AuthDataType, SystemModel } from '../data/system';
import Logger from '../loaders/logger'; import Logger from '../loaders/logger';
import { Dependence } from '../data/dependence';
interface IDependencyFn<T> {
(): Promise<T>;
dependency?: Dependence;
}
class TaskLimit { class TaskLimit {
private dependenyLimit = new PQueue({ concurrency: 1 }); private oneLimit = new PQueue({ concurrency: 1 });
private queuedDependencyIds = new Set<number>([]);
private updateLogLimit = new PQueue({ concurrency: 1 }); private updateLogLimit = new PQueue({ concurrency: 1 });
private cronLimit = new PQueue({ private cronLimit = new PQueue({ concurrency: Math.max(os.cpus().length, 4) });
concurrency: Math.max(os.cpus().length, 4),
});
private manualCronoLimit = new PQueue({
concurrency: Math.max(os.cpus().length, 4),
});
get cronLimitActiveCount() { get cronLimitActiveCount() {
return this.cronLimit.pending; return this.cronLimit.pending;
@@ -27,10 +16,6 @@ class TaskLimit {
return this.cronLimit.size; return this.cronLimit.size;
} }
get firstDependencyId() {
return [...this.queuedDependencyIds.values()][0];
}
constructor() { constructor() {
this.setCustomLimit(); this.setCustomLimit();
this.handleEvents(); this.handleEvents();
@@ -41,19 +26,21 @@ class TaskLimit {
Logger.info( Logger.info(
`[schedule][任务加入队列] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`, `[schedule][任务加入队列] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,
); );
}); })
this.cronLimit.on('active', () => { this.cronLimit.on('active', () => {
Logger.info( Logger.info(
`[schedule][开始处理任务] 运行中任务数: ${ `[schedule][开始处理任务] 运行中任务数: ${this.cronLimitActiveCount + 1}, 等待中任务数: ${this.cronLimitPendingCount}`,
this.cronLimitActiveCount + 1 );
}, 等待中任务数: ${this.cronLimitPendingCount}`, })
this.cronLimit.on('completed', (param) => {
Logger.info(
`[schedule][任务处理成功] 参数 ${JSON.stringify(param)}`,
); );
}); });
this.cronLimit.on('completed', (param) => { this.cronLimit.on('error', error => {
Logger.info(`[schedule][任务处理成功] 参数 ${JSON.stringify(param)}`); Logger.error(
}); `[schedule][任务处理错误] 参数 ${JSON.stringify(error)}`,
this.cronLimit.on('error', (error) => { );
Logger.error(`[schedule][任务处理错误] 参数 ${JSON.stringify(error)}`);
}); });
this.cronLimit.on('next', () => { this.cronLimit.on('next', () => {
Logger.info( Logger.info(
@@ -61,20 +48,15 @@ class TaskLimit {
); );
}); });
this.cronLimit.on('idle', () => { 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) { public async setCustomLimit(limit?: number) {
if (limit) { if (limit) {
this.cronLimit.concurrency = limit; this.cronLimit.concurrency = limit;
this.manualCronoLimit.concurrency = limit;
return; return;
} }
await SystemModel.sync(); await SystemModel.sync();
@@ -83,38 +65,18 @@ class TaskLimit {
}); });
if (doc?.info?.cronConcurrency) { if (doc?.info?.cronConcurrency) {
this.cronLimit.concurrency = doc.info.cronConcurrency; this.cronLimit.concurrency = doc.info.cronConcurrency;
this.manualCronoLimit.concurrency = doc.info.cronConcurrency;
} }
} }
public async runWithCronLimit<T>( public async runWithCronLimit<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
fn: () => Promise<T>,
options?: Partial<QueueAddOptions>,
): Promise<T | void> {
return this.cronLimit.add(fn, options); return this.cronLimit.add(fn, options);
} }
public async manualRunWithCronLimit<T>( public runOneByOne<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
fn: () => Promise<T>, return this.oneLimit.add(fn, options);
options?: Partial<QueueAddOptions>,
): Promise<T | void> {
return this.manualCronoLimit.add(fn, options);
} }
public runDependeny<T>( public updateDepLog<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
dependency: Dependence,
fn: IDependencyFn<T>,
options?: Partial<QueueAddOptions>,
): Promise<T | void> {
this.queuedDependencyIds.add(dependency.id!);
fn.dependency = dependency;
return this.dependenyLimit.add(fn, options);
}
public updateDepLog<T>(
fn: () => Promise<T>,
options?: Partial<QueueAddOptions>,
): Promise<T | void> {
return this.updateLogLimit.add(fn, options); return this.updateLogLimit.add(fn, options);
} }
} }
-27
View File
@@ -1,27 +0,0 @@
import 'reflect-metadata'; // We need this in order to use @Decorators
import config from './config';
import express from 'express';
import depInjectorLoader from './loaders/depInjector';
import Logger from './loaders/logger';
async function startServer() {
const app = express();
depInjectorLoader();
await require('./loaders/update').default({ app });
app
.listen(config.updatePort, '0.0.0.0', () => {
Logger.debug(`✌️ 更新服务启动成功!`);
console.debug(`✌️ 更新服务启动成功!`);
process.send?.('ready');
})
.on('error', (err) => {
Logger.error(err);
console.error(err);
process.exit(1);
});
}
startServer();
-82
View File
@@ -1,82 +0,0 @@
FROM python:3.10-alpine3.18 as builder
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
RUN set -x \
&& apk update \
&& apk add nodejs npm git \
&& npm i -g pnpm@8.3.1 pm2 tsx \
&& cd /tmp/build \
&& pnpm install --prod
FROM python:3.10-alpine
ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}"
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=develop
ENV PNPM_HOME=/root/.local/share/pnpm \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules \
LANG=C.UTF-8 \
SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \
QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH}
VOLUME /ql/data
EXPOSE 5700
COPY --from=builder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/
COPY --from=builder /usr/local/bin/. /usr/local/bin/
RUN set -x \
&& apk update -f \
&& apk upgrade \
&& apk --no-cache add -f bash \
coreutils \
git \
curl \
wget \
tzdata \
perl \
openssl \
nginx \
nodejs \
jq \
openssh \
procps \
netcat-openbsd \
unzip \
npm \
&& rm -rf /var/cache/apk/* \
&& apk update \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \
&& git config --global http.postBuffer 524288000 \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \
&& ulimit -c 0
ARG SOURCE_COMMIT
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& cd ${QL_DIR} \
&& cp -f .env.example .env \
&& chmod 777 ${QL_DIR}/shell/*.sh \
&& chmod 777 ${QL_DIR}/docker/*.sh \
&& git clone --depth=1 -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
&& mkdir -p ${QL_DIR}/static \
&& cp -rf /static/* ${QL_DIR}/static \
&& rm -rf /static
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
WORKDIR ${QL_DIR}
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
CMD curl -sf --noproxy '*' http://127.0.0.1:5400/api/health || exit 1
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+4 -10
View File
@@ -3,11 +3,11 @@ COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
RUN set -x \ RUN set -x \
&& apk update \ && apk update \
&& apk add nodejs npm git \ && apk add nodejs npm git \
&& npm i -g pnpm@8.3.1 pm2 tsx \ && npm i -g pnpm@8.3.1 \
&& cd /tmp/build \ && cd /tmp/build \
&& pnpm install --prod && pnpm install --prod
FROM python:3.11-alpine FROM python:3.11-alpine3.18
ARG QL_MAINTAINER="whyour" ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}" LABEL maintainer="${QL_MAINTAINER}"
@@ -23,13 +23,6 @@ ENV PNPM_HOME=/root/.local/share/pnpm \
QL_DIR=/ql \ QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH} QL_BRANCH=${QL_BRANCH}
VOLUME /ql/data
EXPOSE 5700
COPY --from=builder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/
COPY --from=builder /usr/local/bin/. /usr/local/bin/
RUN set -x \ RUN set -x \
&& apk update -f \ && apk update -f \
&& apk upgrade \ && apk upgrade \
@@ -56,10 +49,11 @@ RUN set -x \
&& git config --global user.email "qinglong@@users.noreply.github.com" \ && git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \ && git config --global user.name "qinglong" \
&& git config --global http.postBuffer 524288000 \ && git config --global http.postBuffer 524288000 \
&& npm install -g pnpm@8.3.1 pm2 tsx \
&& rm -rf /root/.pnpm-store \ && rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \ && rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \ && rm -rf /root/.cache \
&& ulimit -c 0 && rm -rf /root/.npm
ARG SOURCE_COMMIT ARG SOURCE_COMMIT
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \ RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
+1 -1
View File
@@ -3,6 +3,7 @@
dir_shell=/ql/shell dir_shell=/ql/shell
. $dir_shell/env.sh . $dir_shell/env.sh
. $dir_shell/share.sh . $dir_shell/share.sh
link_shell
echo -e "======================1. 检测配置文件========================\n" echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d make_dir /etc/nginx/conf.d
@@ -20,7 +21,6 @@ nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "nginx启动成功...\n" echo -e "nginx启动成功...\n"
echo -e "======================4. 启动pm2服务========================\n" echo -e "======================4. 启动pm2服务========================\n"
reload_update
reload_pm2 reload_pm2
if [[ $AutoStartBot == true ]]; then if [[ $AutoStartBot == true ]]; then
-16
View File
@@ -6,10 +6,6 @@ upstream publicApi {
server 0.0.0.0:5400; server 0.0.0.0:5400;
} }
upstream updateApi {
server 0.0.0.0:5300;
}
map $http_upgrade $connection_upgrade { map $http_upgrade $connection_upgrade {
default keep-alive; default keep-alive;
'websocket' upgrade; 'websocket' upgrade;
@@ -20,18 +16,6 @@ server {
IPV6_CONFIG IPV6_CONFIG
ssl_session_timeout 5m; ssl_session_timeout 5m;
location QL_BASE_URLapi/update/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://updateApi/api/;
proxy_buffering off;
proxy_redirect default;
proxy_connect_timeout 1800;
proxy_send_timeout 1800;
proxy_read_timeout 1800;
}
location QL_BASE_URLapi/public/ { location QL_BASE_URLapi/public/ {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
-13
View File
@@ -1,13 +0,0 @@
module.exports = {
apps: [
{
name: 'update',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
time: true,
script: 'static/build/update.js',
},
],
};
+5 -5
View File
@@ -4,7 +4,6 @@
"start": "concurrently -n w: npm:start:*", "start": "concurrently -n w: npm:start:*",
"start:front": "max dev", "start:front": "max dev",
"start:back": "nodemon", "start:back": "nodemon",
"start:update": "ts-node -P tsconfig.back.json ./back/update.ts",
"start:public": "ts-node -P tsconfig.back.json ./back/public.ts", "start:public": "ts-node -P tsconfig.back.json ./back/public.ts",
"start:rpc": "ts-node -P tsconfig.back.json ./back/schedule/index.ts", "start:rpc": "ts-node -P tsconfig.back.json ./back/schedule/index.ts",
"build:front": "max build", "build:front": "max build",
@@ -12,7 +11,6 @@
"panel": "npm run build:back && node static/build/app.js", "panel": "npm run build:back && node static/build/app.js",
"schedule": "npm run build:back && node static/build/schedule/index.js", "schedule": "npm run build:back && node static/build/schedule/index.js",
"public": "npm run build:back && node static/build/public.js", "public": "npm run build:back && node static/build/public.js",
"update": "npm run build:back && node static/build/update.js",
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true", "gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"postinstall": "max setup 2>/dev/null || true", "postinstall": "max setup 2>/dev/null || true",
@@ -85,13 +83,14 @@
"node-schedule": "^2.1.0", "node-schedule": "^2.1.0",
"nodemailer": "^6.7.2", "nodemailer": "^6.7.2",
"p-queue-cjs": "7.3.4", "p-queue-cjs": "7.3.4",
"protobufjs": "^7.3.0", "protobufjs": "^7.2.3",
"pstree.remy": "^1.1.8", "pstree.remy": "^1.1.8",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"sequelize": "^6.25.5", "sequelize": "^6.25.5",
"serve-handler": "^6.1.3", "serve-handler": "^6.1.3",
"sockjs": "^0.3.24", "sockjs": "^0.3.24",
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3", "sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.4",
"tar": "^6.1.15",
"toad-scheduler": "^1.6.0", "toad-scheduler": "^1.6.0",
"typedi": "^0.10.0", "typedi": "^0.10.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
@@ -129,6 +128,7 @@
"@types/serve-handler": "^6.1.1", "@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33", "@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@types/tar": "^6.1.5",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@types/request-ip": "0.0.41", "@types/request-ip": "0.0.41",
"@uiw/codemirror-extensions-langs": "^4.21.9", "@uiw/codemirror-extensions-langs": "^4.21.9",
@@ -166,7 +166,7 @@
"ts-node": "^10.6.0", "ts-node": "^10.6.0",
"ts-proto": "^1.146.0", "ts-proto": "^1.146.0",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"tsx": "^4.7.3", "tsx": "^3.12.3",
"typescript": "5.2.2", "typescript": "5.2.2",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"virtualizedtableforantd4": "1.3.0", "virtualizedtableforantd4": "1.3.0",
+187 -369
View File
File diff suppressed because it is too large Load Diff
+15 -57
View File
@@ -56,12 +56,6 @@ export BARK_ICON="https://qn.whyour.cn/logo.png"
export BARK_SOUND="" export BARK_SOUND=""
## 下方填写推送消息分组,默认为"QingLong" ## 下方填写推送消息分组,默认为"QingLong"
export BARK_GROUP="QingLong" export BARK_GROUP="QingLong"
## bark 推送时效性
export BARK_LEVEL="active"
## bark 推送是否存档
export BARK_ARCHIVE=""
## bark 推送跳转 URL
export BARK_URL=""
## 3. Telegram ## 3. Telegram
## 下方填写自己申请@BotFather的Token,如10xxx4:AAFcqxxxxgER5uw ## 下方填写自己申请@BotFather的Token,如10xxx4:AAFcqxxxxgER5uw
@@ -118,18 +112,7 @@ export PUSH_PLUS_TOKEN=""
## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送 ## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送
export PUSH_PLUS_USER="" export PUSH_PLUS_USER=""
## 9. 微加机器人 ## 9. go-cqhttp
## 官方网站:http://www.weplusbot.com
## 下方填写您的Token;微信扫描登录后在"我的"->"设置"->"令牌"中获取
export WE_PLUS_BOT_TOKEN=""
## 消息接收人;
## 个人版填写接收消息的群编码,不填发送给自己的微信号
## 专业版不填默认发给机器人自己,发送给好友填写wxid,发送给微信群填写群编码
export WE_PLUS_BOT_RECEIVER=""
## 调用版本;分为专业版和个人版,专业版填写pro,个人版填写personal
export WE_PLUS_BOT_VERSION="pro"
## 10. go-cqhttp
## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg ## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
## gobot_token 填写在go-cqhttp文件设置的访问密钥 ## gobot_token 填写在go-cqhttp文件设置的访问密钥
## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群 ## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
@@ -138,7 +121,7 @@ export GOBOT_URL=""
export GOBOT_TOKEN="" export GOBOT_TOKEN=""
export GOBOT_QQ="" export GOBOT_QQ=""
## 11. gotify ## 10. gotify
## gotify_url 填写gotify地址,如https://push.example.de:8080 ## gotify_url 填写gotify地址,如https://push.example.de:8080
## gotify_token 填写gotify的消息应用token ## gotify_token 填写gotify的消息应用token
## gotify_priority 填写推送消息优先级,默认为0 ## gotify_priority 填写推送消息优先级,默认为0
@@ -146,17 +129,17 @@ export GOTIFY_URL=""
export GOTIFY_TOKEN="" export GOTIFY_TOKEN=""
export GOTIFY_PRIORITY=0 export GOTIFY_PRIORITY=0
## 12. PushDeer ## 11. PushDeer
## deer_key 填写PushDeer的key ## deer_key 填写PushDeer的key
export DEER_KEY="" export DEER_KEY=""
## 13. Chat ## 12. Chat
## chat_url 填写synology chat地址,http://IP:PORT/webapi/***token= ## chat_url 填写synology chat地址,http://IP:PORT/webapi/***token=
## chat_token 填写后面的token ## chat_token 填写后面的token
export CHAT_URL="" export CHAT_URL=""
export CHAT_TOKEN="" export CHAT_TOKEN=""
## 14. aibotk ## 13. aibotk
## 官方说明文档:http://wechat.aibotk.com/oapi/oapi?from=ql ## 官方说明文档:http://wechat.aibotk.com/oapi/oapi?from=ql
## aibotk_key (必填)填写智能微秘书个人中心的apikey ## aibotk_key (必填)填写智能微秘书个人中心的apikey
export AIBOTK_KEY="" export AIBOTK_KEY=""
@@ -165,16 +148,7 @@ export AIBOTK_TYPE=""
## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应 ## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应
export AIBOTK_NAME="" export AIBOTK_NAME=""
## 15. CHRONOCAT ## 14. SMTP
## CHRONOCAT_URL 推送 http://127.0.0.1:16530
## CHRONOCAT_TOKEN 填写在CHRONOCAT文件生成的访问密钥
## CHRONOCAT_QQ 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx
## CHRONOCAT相关API https://chronocat.vercel.app/install/docker/official/
export CHRONOCAT_URL=""
export CHRONOCAT_QQ=""
export CHRONOCAT_TOKEN=""
## 16. SMTP
## 邮箱服务名称,比如126、163、Gmail、QQ等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json ## 邮箱服务名称,比如126、163、Gmail、QQ等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json
export SMTP_SERVICE="" export SMTP_SERVICE=""
## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己 ## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己
@@ -184,34 +158,18 @@ export SMTP_PASSWORD=""
## smtp_name 填写 SMTP 收发件人姓名,可随意填写 ## smtp_name 填写 SMTP 收发件人姓名,可随意填写
export SMTP_NAME="" export SMTP_NAME=""
## 17. PushMe ## 15. PushMe
## 官方说明文档:https://push.i-i.me/ ## 官方说明文档:https://push.i-i.me/
## PUSHME_KEY (必填)填写PushMe APP上获取的push_key ## PUSHME_KEY (必填)填写PushMe APP上获取的push_key
## PUSHME_URL (选填)填写自建的PushMeServer消息服务接口地址,例如:http://127.0.0.1:3010,不填则使用官方接口服务
export PUSHME_KEY="" export PUSHME_KEY=""
export PUSHME_URL=""
## 18. 飞书机器人 ## 13. CHRONOCAT
## 官方文档:https://www.feishu.cn/hc/zh-CN/articles/360024984973 ## CHRONOCAT_URL 推送 http://127.0.0.1:16530
## FSKEY 飞书机器人的 FSKEY ## CHRONOCAT_TOKEN 填写在CHRONOCAT文件生成的访问密钥
export FSKEY="" ## 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/
## 19. Qmsg酱 export CHRONOCAT_URL=""
## 官方文档:https://qmsg.zendee.cn/docs/api/ export CHRONOCAT_QQ=""
## qmsg 酱的 QMSG_KEY export CHRONOCAT_TOKEN=""
## qmsg 酱的 QMSG_TYPE send 为私聊,group 为群聊
export QMSG_KEY=""
export QMSG_TYPE=""
## 20. 自定义通知
## 自定义通知 接收回调的URL
export WEBHOOK_URL=""
## WEBHOOK_BODY 和 WEBHOOK_HEADERS 多个参数时,直接换行或者使用 $'\n' 连接多行字符串,比如 export dd="line 1"$'\n'"line 2"
export WEBHOOK_BODY=""
export WEBHOOK_HEADERS=""
## 支持 GET/POST/PUT
export WEBHOOK_METHOD=""
## 支持 text/plain、application/json、multipart/form-data、application/x-www-form-urlencoded
export WEBHOOK_CONTENT_TYPE=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可 ## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+509 -406
View File
File diff suppressed because one or more lines are too long
+52 -94
View File
@@ -33,7 +33,7 @@ def print(text, *args, **kw):
# 通知服务 # 通知服务
# fmt: off # fmt: off
push_config = { push_config = {
'HITOKOTO': True, # 启用一言(随机句子) 'HITOKOTO': False, # 启用一言(随机句子)
'BARK_PUSH': '', # bark IP 或设备码,例:https://api.day.app/DxHcxxxxxRxxxxxxcm/ 'BARK_PUSH': '', # bark IP 或设备码,例:https://api.day.app/DxHcxxxxxRxxxxxxcm/
'BARK_ARCHIVE': '', # bark 推送是否存档 'BARK_ARCHIVE': '', # bark 推送是否存档
@@ -43,7 +43,7 @@ push_config = {
'BARK_LEVEL': '', # bark 推送时效性 'BARK_LEVEL': '', # bark 推送时效性
'BARK_URL': '', # bark 推送跳转URL 'BARK_URL': '', # bark 推送跳转URL
'CONSOLE': False, # 控制台输出 'CONSOLE': True, # 控制台输出
'DD_BOT_SECRET': '', # 钉钉机器人的 DD_BOT_SECRET 'DD_BOT_SECRET': '', # 钉钉机器人的 DD_BOT_SECRET
'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN 'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN
@@ -75,10 +75,6 @@ push_config = {
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌 'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码 'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
'WE_PLUS_BOT_TOKEN': '', # 微加机器人的用户令牌
'WE_PLUS_BOT_RECEIVER': '', # 微加机器人的消息接收者
'WE_PLUS_BOT_VERSION': 'pro', # 微加机器人的调用版本
'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY 'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE 'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
@@ -105,8 +101,7 @@ push_config = {
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定 'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写 'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
'PUSHME_KEY': '', # PushMe 的 PUSHME_KEY 'PUSHME_KEY': '', # PushMe 的 PUSHME_KEY
'PUSHME_URL': '', # PushMe 的 PUSHME_URL
'CHRONOCAT_QQ': '', # qq号 'CHRONOCAT_QQ': '', # qq号
'CHRONOCAT_TOKEN': '', # CHRONOCAT 的token 'CHRONOCAT_TOKEN': '', # CHRONOCAT 的token
@@ -118,6 +113,7 @@ push_config = {
'WEBHOOK_METHOD': '', # 自定义通知 请求方法 'WEBHOOK_METHOD': '', # 自定义通知 请求方法
'WEBHOOK_CONTENT_TYPE': '' # 自定义通知 content-type 'WEBHOOK_CONTENT_TYPE': '' # 自定义通知 content-type
} }
notify_function = []
# fmt: on # fmt: on
# 首先读取 面板变量 或者 github action 运行变量 # 首先读取 面板变量 或者 github action 运行变量
@@ -218,7 +214,7 @@ def feishu_bot(title: str, content: str) -> None:
data = {"msg_type": "text", "content": {"text": f"{title}\n\n{content}"}} data = {"msg_type": "text", "content": {"text": f"{title}\n\n{content}"}}
response = requests.post(url, data=json.dumps(data)).json() response = requests.post(url, data=json.dumps(data)).json()
if response.get("StatusCode") == 0 or response.get("code") == 0: if response.get("StatusCode") == 0:
print("飞书 推送成功!") print("飞书 推送成功!")
else: else:
print("飞书 推送失败!错误信息如下:\n", response) print("飞书 推送失败!错误信息如下:\n", response)
@@ -385,37 +381,6 @@ def pushplus_bot(title: str, content: str) -> None:
else: else:
print("PUSHPLUS 推送失败!") print("PUSHPLUS 推送失败!")
def weplus_bot(title: str, content: str) -> None:
"""
通过 微加机器人 推送消息。
"""
if not push_config.get("WE_PLUS_BOT_TOKEN"):
print("微加机器人 服务的 WE_PLUS_BOT_TOKEN 未设置!!\n取消推送")
return
print("微加机器人 服务启动")
template = "txt"
if len(content) > 800:
template = "html"
url = "https://www.weplusbot.com/send"
data = {
"token": push_config.get("WE_PLUS_BOT_TOKEN"),
"title": title,
"content": content,
"template": template,
"receiver": push_config.get("WE_PLUS_BOT_RECEIVER"),
"version": push_config.get("WE_PLUS_BOT_VERSION"),
}
body = json.dumps(data).encode(encoding="utf-8")
headers = {"Content-Type": "application/json"}
response = requests.post(url=url, data=body, headers=headers).json()
if response["code"] == 200:
print("微加机器人 推送成功!")
else:
print("微加机器人 推送失败!")
def qmsg_bot(title: str, content: str) -> None: def qmsg_bot(title: str, content: str) -> None:
""" """
@@ -613,9 +578,7 @@ def aibotk(title: str, content: str) -> None:
or not push_config.get("AIBOTK_TYPE") or not push_config.get("AIBOTK_TYPE")
or not push_config.get("AIBOTK_NAME") or not push_config.get("AIBOTK_NAME")
): ):
print( print("智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送")
"智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送"
)
return return
print("智能微秘书 服务启动") print("智能微秘书 服务启动")
@@ -704,13 +667,10 @@ def pushme(title: str, content: str) -> None:
return return
print("PushMe 服务启动") print("PushMe 服务启动")
url = push_config.get("PUSHME_URL") if push_config.get("PUSHME_URL") else "https://push.i-i.me/" url = f'https://push.i-i.me/?push_key={push_config.get("PUSHME_KEY")}'
data = { data = {
"push_key": push_config.get("PUSHME_KEY"),
"title": title, "title": title,
"content": content, "content": content,
"date": push_config.get("date") if push_config.get("date") else "",
"type": push_config.get("type") if push_config.get("type") else "",
} }
response = requests.post(url, data=data) response = requests.post(url, data=data)
@@ -788,29 +748,32 @@ def parse_headers(headers):
return parsed return parsed
def parse_string(input_string, value_format_fn=None): def parse_body(body, content_type):
matches = {}
pattern = r"(\w+):\s*((?:(?!\n\w+:).)*)"
regex = re.compile(pattern)
for match in regex.finditer(input_string):
key, value = match.group(1).strip(), match.group(2).strip()
try:
value = value_format_fn(value) if value_format_fn else value
json_value = json.loads(value)
matches[key] = json_value
except:
matches[key] = value
return matches
def parse_body(body, content_type, value_format_fn=None):
if not body or content_type == "text/plain": if not body or content_type == "text/plain":
return value_format_fn(body) if value_format_fn and body else body return body
parsed = parse_string(body, value_format_fn) parsed = {}
lines = body.split("\n")
for line in lines:
i = line.find(":")
if i == -1:
continue
key = line[:i].strip()
val = line[i + 1 :].strip()
if not key or key in parsed:
continue
try:
json_value = json.loads(val)
parsed[key] = json_value
except:
parsed[key] = val
if content_type == "application/x-www-form-urlencoded": if content_type == "application/x-www-form-urlencoded":
data = urllib.parse.urlencode(parsed, doseq=True) data = urlencode(parsed, doseq=True)
return data return data
if content_type == "application/json": if content_type == "application/json":
@@ -820,6 +783,18 @@ def parse_body(body, content_type, value_format_fn=None):
return parsed return parsed
def format_notify_content(url, body, title, content):
if "$title" not in url and "$title" not in body:
return {}
formatted_url = url.replace("$title", urllib.parse.quote_plus(title)).replace(
"$content", urllib.parse.quote_plus(content)
)
formatted_body = body.replace("$title", title).replace("$content", content)
return formatted_url, formatted_body
def custom_notify(title: str, content: str) -> None: def custom_notify(title: str, content: str) -> None:
""" """
通过 自定义通知 推送消息。 通过 自定义通知 推送消息。
@@ -836,21 +811,18 @@ def custom_notify(title: str, content: str) -> None:
WEBHOOK_BODY = push_config.get("WEBHOOK_BODY") WEBHOOK_BODY = push_config.get("WEBHOOK_BODY")
WEBHOOK_HEADERS = push_config.get("WEBHOOK_HEADERS") WEBHOOK_HEADERS = push_config.get("WEBHOOK_HEADERS")
if "$title" not in WEBHOOK_URL and "$title" not in WEBHOOK_BODY: formatUrl, formatBody = format_notify_content(
WEBHOOK_URL, WEBHOOK_BODY, title, content
)
if not formatUrl and not formatBody:
print("请求头或者请求体中必须包含 $title 和 $content") print("请求头或者请求体中必须包含 $title 和 $content")
return return
headers = parse_headers(WEBHOOK_HEADERS) headers = parse_headers(WEBHOOK_HEADERS)
body = parse_body( body = parse_body(formatBody, WEBHOOK_CONTENT_TYPE)
WEBHOOK_BODY,
WEBHOOK_CONTENT_TYPE,
lambda v: v.replace("$title", title).replace("$content", content),
)
formatted_url = WEBHOOK_URL.replace(
"$title", urllib.parse.quote_plus(title)
).replace("$content", urllib.parse.quote_plus(content))
response = requests.request( response = requests.request(
method=WEBHOOK_METHOD, url=formatted_url, headers=headers, timeout=15, data=body method=WEBHOOK_METHOD, url=formatUrl, headers=headers, timeout=15, data=body
) )
if response.status_code == 200: if response.status_code == 200:
@@ -869,8 +841,6 @@ def one() -> str:
return res["hitokoto"] + " ----" + res["from"] return res["hitokoto"] + " ----" + res["from"]
def add_notify_function():
notify_function = []
if push_config.get("BARK_PUSH"): if push_config.get("BARK_PUSH"):
notify_function.append(bark) notify_function.append(bark)
if push_config.get("CONSOLE"): if push_config.get("CONSOLE"):
@@ -893,8 +863,6 @@ def add_notify_function():
notify_function.append(chat) notify_function.append(chat)
if push_config.get("PUSH_PLUS_TOKEN"): if push_config.get("PUSH_PLUS_TOKEN"):
notify_function.append(pushplus_bot) notify_function.append(pushplus_bot)
if push_config.get("WE_PLUS_BOT_TOKEN"):
notify_function.append(weplus_bot)
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"): if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
notify_function.append(qmsg_bot) notify_function.append(qmsg_bot)
if push_config.get("QYWX_AM"): if push_config.get("QYWX_AM"):
@@ -928,19 +896,8 @@ def add_notify_function():
if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"): if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"):
notify_function.append(custom_notify) notify_function.append(custom_notify)
if not notify_function:
print(f"无推送渠道,请检查通知变量是否正确")
return notify_function
def send(title: str, content: str, ignore_default_config: bool = False, **kwargs):
if kwargs:
global push_config
if ignore_default_config:
push_config = kwargs # 清空从环境变量获取的配置
else:
push_config.update(kwargs)
def send(title: str, content: str) -> None:
if not content: if not content:
print(f"{title} 推送内容为空!") print(f"{title} 推送内容为空!")
return return
@@ -953,9 +910,10 @@ def send(title: str, content: str, ignore_default_config: bool = False, **kwargs
return return
hitokoto = push_config.get("HITOKOTO") hitokoto = push_config.get("HITOKOTO")
content += "\n\n" + one() if hitokoto else ""
notify_function = add_notify_function() text = one() if hitokoto else ""
content += "\n\n" + text
ts = [ ts = [
threading.Thread(target=mode, args=(title, content), name=mode.__name__) threading.Thread(target=mode, args=(title, content), name=mode.__name__)
for mode in notify_function for mode in notify_function
-11
View File
@@ -1,11 +0,0 @@
/**
* 任务名称
* name: script name
* 定时规则
* cron: 1 9 * * *
*/
const { sendNotify } = require('./sendNotify.js'); // commonjs
// import { sendNotify } from './sendNotify'; // es6
console.log('test scripts');
sendNotify('test scripts', 'test desc');
-10
View File
@@ -1,10 +0,0 @@
"""
任务名称
name: script name
定时规则
cron: 1 9 * * *
"""
import notify
print("test script")
notify.send('test script', 'test desc')
-1
View File
@@ -81,7 +81,6 @@ main() {
check_ql check_ql
check_nginx check_nginx
check_pm2 check_pm2
reload_update
reload_pm2 reload_pm2
echo -e "\n=====> 检测结束\n" echo -e "\n=====> 检测结束\n"
} }
+26 -27
View File
@@ -33,12 +33,8 @@ file_task_sample=$dir_sample/task.sample.sh
file_extra_sample=$dir_sample/extra.sample.sh file_extra_sample=$dir_sample/extra.sample.sh
file_notify_js_sample=$dir_sample/notify.js file_notify_js_sample=$dir_sample/notify.js
file_notify_py_sample=$dir_sample/notify.py file_notify_py_sample=$dir_sample/notify.py
file_test_js_sample=$dir_sample/ql_sample.js
file_test_py_sample=$dir_sample/ql_sample.py
file_notify_py=$dir_scripts/notify.py file_notify_py=$dir_scripts/notify.py
file_notify_js=$dir_scripts/sendNotify.js file_notify_js=$dir_scripts/sendNotify.js
file_test_js=$dir_scripts/ql_sample.js
file_test_py=$dir_scripts/ql_sample.py
nginx_app_conf=$dir_root/docker/front.conf nginx_app_conf=$dir_root/docker/front.conf
nginx_conf=$dir_root/docker/nginx.conf nginx_conf=$dir_root/docker/nginx.conf
dep_notify_py=$dir_dep/notify.py dep_notify_py=$dir_dep/notify.py
@@ -129,6 +125,31 @@ gen_random_num() {
echo $((${RANDOM} % $divi)) echo $((${RANDOM} % $divi))
} }
link_shell_sub() {
local link_path="$1"
local original_path="$2"
if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then
rm -f $link_path 2>/dev/null
ln -sf $original_path $link_path
fi
}
link_shell() {
if [[ $is_termux -eq 1 ]]; then
local path="/data/data/com.termux/files/usr/bin/"
elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then
local path="/usr/local/bin/"
else
local path=""
echo -e "脚本功能受限,请自行添加命令的软连接...\n"
fi
if [[ $path ]]; then
for ((i = 0; i < ${#link_name[*]}; i++)); do
link_shell_sub "$path${link_name[i]}" "$dir_shell/${original_name[i]}"
done
fi
}
define_cmd() { define_cmd() {
local cmd_prefix cmd_suffix local cmd_prefix cmd_suffix
if type task &>/dev/null; then if type task &>/dev/null; then
@@ -213,16 +234,6 @@ fix_config() {
echo echo
fi fi
if [[ ! -s $file_test_js ]]; then
cp -fv $file_test_js_sample $file_test_js
echo
fi
if [[ ! -s $file_test_py ]]; then
cp -fv $file_test_py_sample $file_test_py
echo
fi
if [[ -s /etc/nginx/conf.d/default.conf ]]; then if [[ -s /etc/nginx/conf.d/default.conf ]]; then
echo -e "检测到默认nginx配置文件,清空...\n" echo -e "检测到默认nginx配置文件,清空...\n"
cat /dev/null >/etc/nginx/conf.d/default.conf cat /dev/null >/etc/nginx/conf.d/default.conf
@@ -282,7 +293,7 @@ git_clone_scripts() {
set_proxy "$proxy" set_proxy "$proxy"
git clone -q --depth=1 $part_cmd $url $dir git clone --depth=1 $part_cmd $url $dir
exit_status=$? exit_status=$?
unset_proxy unset_proxy
@@ -294,11 +305,6 @@ random_range() {
echo $((RANDOM % ($end - $beg) + $beg)) echo $((RANDOM % ($end - $beg) + $beg))
} }
delete_pm2() {
cd $dir_root
pm2 delete ecosystem.config.js
}
reload_pm2() { reload_pm2() {
cd $dir_root cd $dir_root
restore_env_vars restore_env_vars
@@ -306,13 +312,6 @@ reload_pm2() {
pm2 startOrGracefulReload ecosystem.config.js pm2 startOrGracefulReload ecosystem.config.js
} }
reload_update() {
cd $dir_root
restore_env_vars
pm2 flush &>/dev/null
pm2 startOrGracefulReload other.config.js
}
diff_time() { diff_time() {
local format="$1" local format="$1"
local begin_time="$2" local begin_time="$2"
+24 -36
View File
@@ -34,6 +34,7 @@ output_list_add_drop() {
if [[ -s $list ]]; then if [[ -s $list ]]; then
echo -e "检测到有${type}的定时任务:" echo -e "检测到有${type}的定时任务:"
cat $list cat $list
echo
fi fi
} }
@@ -44,7 +45,7 @@ del_cron() {
local path=$2 local path=$2
local detail="" local detail=""
local ids="" local ids=""
echo -e "\n开始尝试自动删除失效的定时任务..." echo -e "开始尝试自动删除失效的定时任务..."
for cron in $(cat $list_drop); do for cron in $(cat $list_drop); do
local id=$(cat $list_crontab_user | grep -E "$cmd_task.* $cron" | perl -pe "s|.*ID=(.*) $cmd_task.* $cron\.*|\1|" | head -1 | awk -F " " '{print $1}') local id=$(cat $list_crontab_user | grep -E "$cmd_task.* $cron" | perl -pe "s|.*ID=(.*) $cmd_task.* $cron\.*|\1|" | head -1 | awk -F " " '{print $1}')
if [[ $ids ]]; then if [[ $ids ]]; then
@@ -75,7 +76,7 @@ del_cron() {
add_cron() { add_cron() {
local list_add=$1 local list_add=$1
local path=$2 local path=$2
echo -e "\n开始尝试自动添加定时任务..." echo -e "开始尝试自动添加定时任务..."
local detail="" local detail=""
cd $dir_scripts cd $dir_scripts
for file in $(cat $list_add); do for file in $(cat $list_add); do
@@ -85,20 +86,19 @@ add_cron() {
cron_line=$( cron_line=$(
perl -ne "{ perl -ne "{
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/ print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/
}" $file 2>/dev/null | }" $file |
perl -pe "{ perl -pe "{
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$file_name.*|\1|g; s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$file_name.*|\1|g;
s|\*([\d\*])(.*)|\1\2|g; s|\*([\d\*])(.*)|\1\2|g;
s| | |g; s| | |g;
}" 2>/dev/null | sort -u | head -1 }" | sort -u | head -1
) )
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
[[ -z $cron_name ]] && cron_name="$file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron" [[ -z $cron_line ]] && cron_line="$default_cron"
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1) result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name:$SUB_ID")
[[ -z $cron_name ]] && cron_name=$(grep "name:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_name ]] && cron_name=$(basename "$file_name")
result=$(add_cron_api "${cron_line}:${cmd_task} ${file}:${cron_name}:${SUB_ID}")
echo -e "$result" echo -e "$result"
if [[ $detail ]]; then if [[ $detail ]]; then
detail="${detail}${result}\n" detail="${detail}${result}\n"
@@ -135,10 +135,10 @@ update_repo() {
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}" git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}"
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "拉取 ${uniq_path} 成功...\n" echo -e "\n拉取 ${uniq_path} 成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" "$autoAddCron" "$autoDelCron" diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" "$autoAddCron" "$autoDelCron"
else else
echo -e "拉取 ${uniq_path} 失败,请检查日志...\n" echo -e "\n拉取 ${uniq_path} 失败,请检查日志...\n"
fi fi
} }
@@ -195,7 +195,7 @@ update_raw() {
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron" [[ -z $cron_line ]] && cron_line="$default_cron"
result=$(add_cron_api "${cron_line}:${cmd_task} ${filename}:${cron_name}:${SUB_ID}") result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name:$SUB_ID")
echo -e "$result\n" echo -e "$result\n"
notify_api "新增任务通知" "\n$result" notify_api "新增任务通知" "\n$result"
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id" # update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
@@ -231,25 +231,22 @@ usage() {
} }
reload_qinglong() { reload_qinglong() {
delete_pm2
local reload_target="${1}" local reload_target="${1}"
local primary_branch="master" local primary_branch="master"
if [[ "${QL_BRANCH}" == "develop" ]] || [[ "${QL_BRANCH}" == "debian" ]] || [[ "${QL_BRANCH}" == "debian-dev" ]]; then if [[ "${QL_BRANCH}" == "develop" ]]; then
primary_branch="${QL_BRANCH}" primary_branch="develop"
fi fi
if [[ "$reload_target" == 'system' ]]; then if [[ "$reload_target" == 'system' ]]; then
rm -rf ${dir_root}/back ${dir_root}/cli ${dir_root}/docker ${dir_root}/sample ${dir_root}/shell ${dir_root}/src cp -rf ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/
mv -f ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/
rm -rf $dir_static/* rm -rf $dir_static/*
mv -f ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/ cp -rf ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/
cp -f $file_config_sample $dir_config/config.sample.sh cp -f $file_config_sample $dir_config/config.sample.sh
fi fi
if [[ "$reload_target" == 'data' ]]; then if [[ "$reload_target" == 'data' ]]; then
rm -rf ${dir_root}/data/* rm -rf ${dir_root}/data
mv -f ${dir_tmp}/data/* ${dir_root}/data/ cp -rf ${dir_tmp}/data ${dir_root}/
fi fi
reload_pm2 reload_pm2
@@ -261,7 +258,7 @@ update_qinglong() {
local mirror="gitee" local mirror="gitee"
local downloadQLUrl="https://gitee.com/whyour/qinglong/repository/archive" local downloadQLUrl="https://gitee.com/whyour/qinglong/repository/archive"
local downloadStaticUrl="https://gitee.com/whyour/qinglong-static/repository/archive" local downloadStaticUrl="https://gitee.com/whyour/qinglong-static/repository/archive"
local githubStatus=$(curl -s --noproxy "*" -m 2 -IL "https://google.com" | grep 200) local githubStatus=$(curl -s -m 2 -IL "https://google.com" | grep 200)
if [[ ! -z $githubStatus ]]; then if [[ ! -z $githubStatus ]]; then
mirror="github" mirror="github"
downloadQLUrl="https://github.com/whyour/qinglong/archive/refs/heads" downloadQLUrl="https://github.com/whyour/qinglong/archive/refs/heads"
@@ -270,8 +267,8 @@ update_qinglong() {
echo -e "使用 ${mirror} 源更新...\n" echo -e "使用 ${mirror} 源更新...\n"
local primary_branch="master" local primary_branch="master"
if [[ "${QL_BRANCH}" == "develop" ]] || [[ "${QL_BRANCH}" == "debian" ]] || [[ "${QL_BRANCH}" == "debian-dev" ]]; then if [[ "${QL_BRANCH}" == "develop" ]]; then
primary_branch="${QL_BRANCH}" primary_branch="develop"
fi fi
wget -cqO "${dir_tmp}/ql.zip" "${downloadQLUrl}/${primary_branch}.zip" wget -cqO "${dir_tmp}/ql.zip" "${downloadQLUrl}/${primary_branch}.zip"
@@ -313,12 +310,9 @@ check_update_dep() {
echo -e "更新包下载成功..." echo -e "更新包下载成功..."
if [[ "$needRestart" == 'true' ]]; then if [[ "$needRestart" == 'true' ]]; then
delete_pm2 cp -rf ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/
rm -rf ${dir_root}/back ${dir_root}/cli ${dir_root}/docker ${dir_root}/sample ${dir_root}/shell ${dir_root}/src
mv -f ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/
rm -rf $dir_static/* rm -rf $dir_static/*
mv -f ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/ cp -rf ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/
cp -f $file_config_sample $dir_config/config.sample.sh cp -f $file_config_sample $dir_config/config.sample.sh
reload_pm2 reload_pm2
@@ -423,15 +417,9 @@ gen_list_repo() {
fi fi
for file in ${files}; do for file in ${files}; do
dirPath=$(dirname "$file")
filename=$(basename "$file") filename=$(basename "$file")
filePath="${uniq_path}/${filename}" cp -f $file "$dir_scripts/${uniq_path}/${filename}"
if [[ $dirPath ]] && [[ $dirPath != '.' ]]; then echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
mkdir -p "${dir_scripts}/${uniq_path}/${dirPath}"
filePath="${uniq_path}/${dirPath}/${filename}"
fi
cp -f $file "${dir_scripts}/$filePath"
echo "$filePath" >>"$dir_list_tmp/${uniq_path}_scripts.list"
# cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task.* ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}') # cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task.* ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
# if [[ $cron_id ]]; then # if [[ $cron_id ]]; then
# result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id") # result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
+1 -1
View File
@@ -1,7 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons'; import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({ const IconFont = createFromIconfontCN({
scriptUrl: ['//at.alicdn.com/t/c/font_3354854_lc939gab1iq.js'], scriptUrl: ['//at.alicdn.com/t/c/font_3354854_ob5y15ewlyq.js'],
}); });
export default IconFont; export default IconFont;
+2 -12
View File
@@ -100,14 +100,12 @@
"删除中": "Deleting", "删除中": "Deleting",
"已删除": "Deleted", "已删除": "Deleted",
"删除失败": "Deletion Failed", "删除失败": "Deletion Failed",
"已取消": "Cancelled",
"序号": "Number", "序号": "Number",
"备注": "Remarks", "备注": "Remarks",
"更新时间": "Update Time", "更新时间": "Update Time",
"创建时间": "Creation Time", "创建时间": "Creation Time",
"确认删除依赖": "Confirm to delete the dependency", "确认删除依赖": "Confirm to delete the dependency",
"确认重新安装": "Confirm to reinstall", "确认重新安装": "Confirm to reinstall",
"确认取消安装": "Confirm to cancel install",
"确认删除选中的依赖吗": "Confirm to delete the selected dependencies?", "确认删除选中的依赖吗": "Confirm to delete the selected dependencies?",
"确认重新安装选中的依赖吗": "Confirm to reinstall the selected dependencies?", "确认重新安装选中的依赖吗": "Confirm to reinstall the selected dependencies?",
"请输入名称": "Please enter a name", "请输入名称": "Please enter a name",
@@ -239,7 +237,6 @@
"系统将在": "The system will restart in", "系统将在": "The system will restart in",
"秒后自动刷新": "seconds and automatically refresh", "秒后自动刷新": "seconds and automatically refresh",
"检查更新": "Check for Updates", "检查更新": "Check for Updates",
"重新启动": "Reboot",
"确认删除应用": "Confirm to delete the application", "确认删除应用": "Confirm to delete the application",
"确认重置": "Confirm to reset", "确认重置": "Confirm to reset",
"确认重置应用": "Confirm to reset the application", "确认重置应用": "Confirm to reset the application",
@@ -336,7 +333,6 @@
"企业微信应用": "WeChat Work App", "企业微信应用": "WeChat Work App",
"智能微秘书": "Smart WeChat Assistant", "智能微秘书": "Smart WeChat Assistant",
"群晖chat": "Synology Chat", "群晖chat": "Synology Chat",
"微加机器人": "WePlusBot",
"邮箱": "Email", "邮箱": "Email",
"飞书机器人": "Feishu Bot", "飞书机器人": "Feishu Bot",
"自定义通知": "Custom Notification", "自定义通知": "Custom Notification",
@@ -358,7 +354,6 @@
"BARK推送消息的分组,默认为qinglong": "BARK push message grouping, default is qinglong", "BARK推送消息的分组,默认为qinglong": "BARK push message grouping, default is qinglong",
"BARK推送消息的时效性,默认为active": "BARK push message redirecting URL", "BARK推送消息的时效性,默认为active": "BARK push message redirecting URL",
"BARK推送消息的跳转URL": "BARK push message grouping, default is qinglong", "BARK推送消息的跳转URL": "BARK push message grouping, default is qinglong",
"BARK是否保存推送消息": "Does BARK save push messages",
"telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw": "Telegram Bot token, e.g., 1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw", "telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw": "Telegram Bot token, e.g., 1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw",
"telegram用户的id,例如:129xxx206": "Telegram user ID, e.g., 129xxx206", "telegram用户的id,例如:129xxx206": "Telegram user ID, e.g., 129xxx206",
"代理IP": "Proxy IP", "代理IP": "Proxy IP",
@@ -379,15 +374,11 @@
"iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX": "iGot information push key, e.g., https://push.hellyw.com/XXXXXXXX", "iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX": "iGot information push key, e.g., https://push.hellyw.com/XXXXXXXX",
"微信扫码登录后一对一推送或一对多推送下面的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/", "微信扫码登录后一对一推送或一对多推送下面的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.", "一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)": "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.weplusbot.com/": "Token, which can be obtained after scanning and logging in, is available under 'My Account' -> 'Settings' -> 'Tokens'. Please refer to the instructions for detailed steps: https://www.weplusbot.com/",
"消息接收人": "message recipient",
"调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版": "Version, you can specify 'pro' for the Professional version and 'personal' for the Personal version. If left blank, it will default to the Professional version.",
"飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973": "Feishu group bot: https://www.feishu.cn/hc/zh-CN/articles/360024984973", "飞书群组机器人: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://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", "邮箱服务名称,比如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", "邮箱地址": "Email Address",
"SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定": "The SMTP login password may also be a special passphrase, depending on the specific email service provider's instructions", "SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定": "The SMTP login password may also be a special passphrase, depending on the specific email service provider's instructions",
"PushMe的Keyhttps://push.i-i.me/": "PushMe key, https://push.i-i.me/", "PushMe的Keyhttps://push.i-i.me/": "PushMe key, https://push.i-i.me/",
"自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口": "The self built PushMeServer message interface address, for example: http://127.0.0.1:3010 If left blank, use the official message interface",
"请求方法": "Request Method", "请求方法": "Request Method",
"请求头Content-Type": "Request Header Content-Type", "请求头Content-Type": "Request Header Content-Type",
"请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置": "Request URL should start with http or https. URL or body must contain $title, $content is optional and corresponds to the API content position.", "请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置": "Request URL should start with http or https. URL or body must contain $title, $content is optional and corresponds to the API content position.",
@@ -402,7 +393,6 @@
"系统": "System", "系统": "System",
"个人": "Personal", "个人": "Personal",
"重新安装": "Reinstall", "重新安装": "Reinstall",
"取消安装": "Cancel Install",
"强制删除": "Force Delete", "强制删除": "Force Delete",
"全部任务": "All Tasks", "全部任务": "All Tasks",
"关联订阅": "Associate Subscription", "关联订阅": "Associate Subscription",
@@ -432,7 +422,7 @@
"创建变量成功": "Variable created successfully", "创建变量成功": "Variable created successfully",
"编辑变量": "Edit Variable", "编辑变量": "Edit Variable",
"加载中...": "Loading...", "加载中...": "Loading...",
"夹下所日志": "Folder and All Subfiles", "夹下所日志": "Folder and All Subfiles",
"创建文件夹成功": "Folder created successfully", "创建文件夹成功": "Folder created successfully",
"创建文件成功": "File created successfully", "创建文件成功": "File created successfully",
"夹及其子文件": "Folder and Its Subfiles", "夹及其子文件": "Folder and Its Subfiles",
@@ -485,6 +475,6 @@
"代理地址, 支持HTTP(S)/SOCK5": "Proxy Address, supports HTTP(S)/SOCK5", "代理地址, 支持HTTP(S)/SOCK5": "Proxy Address, supports HTTP(S)/SOCK5",
"NPM 镜像源": "NPM Mirror Source", "NPM 镜像源": "NPM Mirror Source",
"PyPI 镜像源": "PyPI Mirror Source", "PyPI 镜像源": "PyPI Mirror Source",
"alpine linux 镜像源": "Alpine Linux Mirror Source", "alpine linux 镜像源, 例如 https://mirrors.aliyun.com": "Alpine Linux Mirror Source, e.g. https://mirrors.aliyun.com",
"如果恢复失败,可进入容器执行": "If recovery fails, you can enter the container and execute" "如果恢复失败,可进入容器执行": "If recovery fails, you can enter the container and execute"
} }
+2 -12
View File
@@ -100,14 +100,12 @@
"删除中": "删除中", "删除中": "删除中",
"已删除": "已删除", "已删除": "已删除",
"删除失败": "删除失败", "删除失败": "删除失败",
"已取消": "已取消",
"序号": "序号", "序号": "序号",
"备注": "备注", "备注": "备注",
"更新时间": "更新时间", "更新时间": "更新时间",
"创建时间": "创建时间", "创建时间": "创建时间",
"确认删除依赖": "确认删除依赖", "确认删除依赖": "确认删除依赖",
"确认重新安装": "确认重新安装", "确认重新安装": "确认重新安装",
"确认取消安装": "确认取消安装",
"确认删除选中的依赖吗": "确认删除选中的依赖吗", "确认删除选中的依赖吗": "确认删除选中的依赖吗",
"确认重新安装选中的依赖吗": "确认重新安装选中的依赖吗", "确认重新安装选中的依赖吗": "确认重新安装选中的依赖吗",
"请输入名称": "请输入名称", "请输入名称": "请输入名称",
@@ -239,7 +237,6 @@
"系统将在": "系统将在", "系统将在": "系统将在",
"秒后自动刷新": "秒后自动刷新", "秒后自动刷新": "秒后自动刷新",
"检查更新": "检查更新", "检查更新": "检查更新",
"重新启动": "重新启动",
"确认删除应用": "确认删除应用", "确认删除应用": "确认删除应用",
"确认重置": "确认重置", "确认重置": "确认重置",
"确认重置应用": "确认重置应用", "确认重置应用": "确认重置应用",
@@ -336,7 +333,6 @@
"企业微信应用": "企业微信应用", "企业微信应用": "企业微信应用",
"智能微秘书": "智能微秘书", "智能微秘书": "智能微秘书",
"群晖chat": "群晖chat", "群晖chat": "群晖chat",
"微加机器人": "微加机器人",
"邮箱": "邮箱", "邮箱": "邮箱",
"飞书机器人": "飞书机器人", "飞书机器人": "飞书机器人",
"自定义通知": "自定义通知", "自定义通知": "自定义通知",
@@ -358,7 +354,6 @@
"BARK推送消息的分组,默认为qinglong": "BARK推送消息的分组,默认为qinglong", "BARK推送消息的分组,默认为qinglong": "BARK推送消息的分组,默认为qinglong",
"BARK推送消息的时效性,默认为active": "BARK推送消息的时效性,默认为active", "BARK推送消息的时效性,默认为active": "BARK推送消息的时效性,默认为active",
"BARK推送消息的跳转URL": "BARK推送消息的跳转URL", "BARK推送消息的跳转URL": "BARK推送消息的跳转URL",
"BARK是否保存推送消息": "BARK是否保存推送消息",
"telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw": "telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw", "telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw": "telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw",
"telegram用户的id,例如:129xxx206": "telegram用户的id,例如:129xxx206", "telegram用户的id,例如:129xxx206": "telegram用户的id,例如:129xxx206",
"代理IP": "代理IP", "代理IP": "代理IP",
@@ -379,15 +374,11 @@
"iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX": "iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX", "iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX": "iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX",
"微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/": "微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/", "微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/": "微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/",
"一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)": "一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)", "一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)": "一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)",
"用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 https://www.weplusbot.com/": "用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 https://www.weplusbot.com/",
"消息接收人": "消息接收人",
"调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版": "调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版",
"飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973": "飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973", "飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973": "飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973",
"邮箱服务名称,比如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", "邮箱服务名称,比如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/", "PushMe的Keyhttps://push.i-i.me/": "PushMe的Keyhttps://push.i-i.me/",
"自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口": "自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口",
"请求方法": "请求方法", "请求方法": "请求方法",
"请求头Content-Type": "请求头Content-Type", "请求头Content-Type": "请求头Content-Type",
"请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置": "请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置", "请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置": "请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置",
@@ -402,7 +393,6 @@
"系统": "系统", "系统": "系统",
"个人": "个人", "个人": "个人",
"重新安装": "重新安装", "重新安装": "重新安装",
"取消安装": "取消安装",
"强制删除": "强制删除", "强制删除": "强制删除",
"全部任务": "全部任务", "全部任务": "全部任务",
"关联订阅": "关联订阅", "关联订阅": "关联订阅",
@@ -432,7 +422,7 @@
"创建变量成功": "创建变量成功", "创建变量成功": "创建变量成功",
"编辑变量": "编辑变量", "编辑变量": "编辑变量",
"加载中...": "加载中...", "加载中...": "加载中...",
"夹下所日志": "夹下所日志", "夹下所日志": "夹下所日志",
"创建文件夹成功": "创建文件夹成功", "创建文件夹成功": "创建文件夹成功",
"创建文件成功": "创建文件成功", "创建文件成功": "创建文件成功",
"夹及其子文件": "夹及其子文件", "夹及其子文件": "夹及其子文件",
@@ -485,6 +475,6 @@
"代理地址, 支持HTTP(S)/SOCK5": "代理地址, 支持HTTP(S)/SOCK5", "代理地址, 支持HTTP(S)/SOCK5": "代理地址, 支持HTTP(S)/SOCK5",
"NPM 镜像源": "NPM 镜像源", "NPM 镜像源": "NPM 镜像源",
"PyPI 镜像源": "PyPI 镜像源", "PyPI 镜像源": "PyPI 镜像源",
"alpine linux 镜像源": "alpine linux 镜像源", "alpine linux 镜像源, 例如 https://mirrors.aliyun.com": "alpine linux 镜像源, 例如 https://mirrors.aliyun.com",
"如果恢复失败,可进入容器执行": "如果恢复失败,可进入容器执行" "如果恢复失败,可进入容器执行": "如果恢复失败,可进入容器执行"
} }
+1 -2
View File
@@ -76,7 +76,7 @@ const Config = () => {
}; };
useHotkeys( useHotkeys(
'mod+s', 'meta+s',
(e) => { (e) => {
updateConfig(); updateConfig();
}, },
@@ -136,7 +136,6 @@ const Config = () => {
lineNumbersMinChars: 3, lineNumbersMinChars: 3,
folding: false, folding: false,
glyphMargin: false, glyphMargin: false,
accessibilitySupport: 'off'
}} }}
onMount={(editor) => { onMount={(editor) => {
editorRef.current = editor; editorRef.current = editor;
+12 -16
View File
@@ -110,7 +110,6 @@ const CronDetailModal = ({
minimap: { enabled: false }, minimap: { enabled: false },
lineNumbersMinChars: 3, lineNumbersMinChars: 3,
glyphMargin: false, glyphMargin: false,
accessibilitySupport: 'off',
}} }}
onMount={(editor, monaco) => { onMount={(editor, monaco) => {
editorRef.current = editor; editorRef.current = editor;
@@ -120,12 +119,16 @@ const CronDetailModal = ({
}; };
const onClickItem = (item: LogItem) => { const onClickItem = (item: LogItem) => {
const url = `${config.apiPrefix}logs/detail?file=${item.filename}&path=${ const url = `${config.apiPrefix}logs/${item.filename}?path=${
item.directory || '' item.directory || ''
}`; }`;
localStorage.setItem('logCron', url); localStorage.setItem('logCron', url);
setLogUrl(url); setLogUrl(url);
request.get(url).then(({ code, data }) => { request
.get(
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
)
.then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setLog(data); setLog(data);
setIsLogModalVisible(true); setIsLogModalVisible(true);
@@ -156,15 +159,14 @@ const CronDetailModal = ({
const [s, p] = result; const [s, p] = result;
setScriptInfo({ parent: p, filename: s }); setScriptInfo({ parent: p, filename: s });
request request
.get(`${config.apiPrefix}scripts/detail?file=${s}&path=${p || ''}`) .get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setValue(data); setValue(data);
} }
}); });
} else { } else if (result) {
setValidTabs([validTabs[0]]); setValidTabs([validTabs[0]]);
setActiveTabKey('log');
} }
}; };
@@ -175,9 +177,8 @@ const CronDetailModal = ({
<> <>
{intl.get('确认保存文件')} {intl.get('确认保存文件')}
<Text style={{ wordBreak: 'break-all' }} type="warning"> <Text style={{ wordBreak: 'break-all' }} type="warning">
{' '}
{scriptInfo.filename} {scriptInfo.filename}
</Text> </Text>{' '}
{intl.get(',保存后不可恢复')} {intl.get(',保存后不可恢复')}
</> </>
), ),
@@ -360,15 +361,10 @@ const CronDetailModal = ({
<Modal <Modal
title={ title={
<div className="crontab-title-wrapper"> <div className="crontab-title-wrapper">
<div style={{ minWidth: 0, display: 'flex', alignItems: 'center' }}> <div style={{ minWidth: 0 }}>
<Typography.Text <Typography.Text
style={{ width: 200, boxSizing: 'content-box' }} style={{ width: '100%' }}
ellipsis={{ ellipsis={{ tooltip: currentCron.name }}
onEllipsis(ellipsis) {
return ellipsis;
},
tooltip: currentCron.name,
}}
> >
{currentCron.name} {currentCron.name}
</Typography.Text> </Typography.Text>
-1
View File
@@ -81,7 +81,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 24px;
.operations { .operations {
display: flex; display: flex;
+5 -3
View File
@@ -75,16 +75,18 @@ const Crontab = () => {
style={{ style={{
wordBreak: 'break-all', wordBreak: 'break-all',
marginBottom: 0, marginBottom: 0,
color: '#1890ff', color: '#1890ff'
cursor: 'pointer'
}} }}
ellipsis={{ tooltip: text, rows: 2 }} ellipsis={{ tooltip: text, rows: 2 }}
>
<Link
onClick={() => { onClick={() => {
setDetailCron(record); setDetailCron(record);
setIsDetailModalVisible(true); setIsDetailModalVisible(true);
}} }}
> >
<Link>{record.name || '-'}</Link> {record.name || '-'}
</Link>
</Paragraph> </Paragraph>
), ),
sorter: { sorter: {
+15 -89
View File
@@ -22,7 +22,6 @@ import {
FileTextOutlined, FileTextOutlined,
CloseCircleOutlined, CloseCircleOutlined,
ClockCircleOutlined, ClockCircleOutlined,
MinusCircleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
@@ -37,12 +36,20 @@ import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight'; import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import WebSocketManager from '@/utils/websocket'; import WebSocketManager from '@/utils/websocket';
import { DependenceStatus, Status } from './type';
import IconFont from '@/components/iconfont';
const { Text } = Typography; const { Text } = Typography;
const { Search } = Input; const { Search } = Input;
enum Status {
'安装中',
'已安装',
'安装失败',
'删除中',
'已删除',
'删除失败',
'队列中',
}
enum StatusColor { enum StatusColor {
'processing', 'processing',
'success', 'success',
@@ -78,10 +85,6 @@ const StatusMap: Record<number, { icon: React.ReactNode; color: string }> = {
icon: <ClockCircleOutlined />, icon: <ClockCircleOutlined />,
color: 'default', color: 'default',
}, },
7: {
icon: <MinusCircleOutlined />,
color: 'default',
},
}; };
const Dependence = () => { const Dependence = () => {
@@ -105,40 +108,6 @@ const Dependence = () => {
key: 'status', key: 'status',
width: 120, width: 120,
dataIndex: 'status', 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) => { render: (text: string, record: any, index: number) => {
return ( return (
<Space size="middle" style={{ cursor: 'text' }}> <Space size="middle" style={{ cursor: 'text' }}>
@@ -185,7 +154,6 @@ const Dependence = () => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
<Space size="middle"> <Space size="middle">
{![Status., Status.].includes(record.status) && (
<Tooltip title={isPc ? intl.get('日志') : ''}> <Tooltip title={isPc ? intl.get('日志') : ''}>
<a <a
onClick={() => { onClick={() => {
@@ -195,29 +163,19 @@ const Dependence = () => {
<FileTextOutlined /> <FileTextOutlined />
</a> </a>
</Tooltip> </Tooltip>
)} {record.status !== Status. &&
{[Status., Status., Status.].includes( record.status !== Status. && (
record.status,
) ? (
<Tooltip title={isPc ? intl.get('取消安装') : ''}>
<a onClick={() => cancelDependence(record)}>
<IconFont type="ql-icon-quxiaoanzhuang" />
</a>
</Tooltip>
) : (
<> <>
<Tooltip title={isPc ? intl.get('重新安装') : ''}> <Tooltip title={isPc ? intl.get('重新安装') : ''}>
<a onClick={() => reInstallDependence(record, index)}> <a onClick={() => reInstallDependence(record, index)}>
<BugOutlined /> <BugOutlined />
</a> </a>
</Tooltip> </Tooltip>
{Status. === record.status && (
<Tooltip title={isPc ? intl.get('删除') : ''}> <Tooltip title={isPc ? intl.get('删除') : ''}>
<a onClick={() => deleteDependence(record, index)}> <a onClick={() => deleteDependence(record, index)}>
<DeleteOutlined /> <DeleteOutlined />
</a> </a>
</Tooltip> </Tooltip>
)}
<Tooltip title={isPc ? intl.get('强制删除') : ''}> <Tooltip title={isPc ? intl.get('强制删除') : ''}>
<a onClick={() => deleteDependence(record, index, true)}> <a onClick={() => deleteDependence(record, index, true)}>
<DeleteFilled /> <DeleteFilled />
@@ -242,15 +200,11 @@ const Dependence = () => {
const tableRef = useRef<HTMLDivElement>(null); const tableRef = useRef<HTMLDivElement>(null);
const tableScrollHeight = useTableScrollHeight(tableRef, 59); const tableScrollHeight = useTableScrollHeight(tableRef, 59);
const getDependencies = (status?: number[]) => { const getDependencies = () => {
setLoading(true); setLoading(true);
request request
.get( .get(
`${ `${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`,
config.apiPrefix
}dependencies?searchValue=${searchText}&type=${type}&status=${
status || ''
}`,
) )
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
@@ -335,31 +289,6 @@ 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[]) => { const handleCancel = (dependence?: any[]) => {
setIsModalVisible(false); setIsModalVisible(false);
dependence && handleDependence(dependence); dependence && handleDependence(dependence);
@@ -491,7 +420,7 @@ const Dependence = () => {
} }
return _result; return _result;
}); });
}, 300); }, 5000);
return; return;
} }
} }
@@ -609,9 +538,6 @@ const Dependence = () => {
size="middle" size="middle"
scroll={{ x: 768, y: tableScrollHeight }} scroll={{ x: 768, y: tableScrollHeight }}
loading={loading} loading={loading}
onChange={(pagination, filters) => {
getDependencies(filters?.status as number[]);
}}
/> />
</DndProvider> </DndProvider>
</div> </div>
+2 -9
View File
@@ -10,7 +10,6 @@ import {
import { PageLoading } from '@ant-design/pro-layout'; import { PageLoading } from '@ant-design/pro-layout';
import Ansi from 'ansi-to-react'; import Ansi from 'ansi-to-react';
import WebSocketManager from '@/utils/websocket'; import WebSocketManager from '@/utils/websocket';
import { Status } from './type';
const DependenceLogModal = ({ const DependenceLogModal = ({
dependence, dependence,
@@ -97,11 +96,7 @@ const DependenceLogModal = ({
const handleMessage = (payload: any) => { const handleMessage = (payload: any) => {
const { message, references } = payload; const { message, references } = payload;
if ( if (references.length > 0 && references.includes(dependence.id)) {
references.length > 0 &&
references.includes(dependence.id) &&
[Status., Status.].includes(dependence.status)
) {
if (message.includes('结束时间')) { if (message.includes('结束时间')) {
setExecuting(false); setExecuting(false);
setIsRemoveFailed(message.includes('删除失败')); setIsRemoveFailed(message.includes('删除失败'));
@@ -113,13 +108,11 @@ const DependenceLogModal = ({
useEffect(() => { useEffect(() => {
const ws = WebSocketManager.getInstance(); const ws = WebSocketManager.getInstance();
ws.subscribe('installDependence', handleMessage); ws.subscribe('installDependence', handleMessage);
ws.subscribe('uninstallDependence', handleMessage);
return () => { return () => {
ws.unsubscribe('installDependence', handleMessage); ws.unsubscribe('installDependence', handleMessage);
ws.unsubscribe('uninstallDependence', handleMessage);
}; };
}, [dependence]); }, []);
useEffect(() => { useEffect(() => {
setIsPhone(document.body.clientWidth < 768); setIsPhone(document.body.clientWidth < 768);
-21
View File
@@ -1,21 +0,0 @@
export enum DependenceStatus {
'installing',
'installed',
'installFailed',
'removing',
'removed',
'removeFailed',
'queued',
'cancelled',
}
export enum Status {
'安装中',
'已安装',
'安装失败',
'删除中',
'已删除',
'删除失败',
'队列中',
'已取消',
}
+6 -11
View File
@@ -55,11 +55,7 @@ const Log = () => {
const getLog = (node: any) => { const getLog = (node: any) => {
request request
.get( .get(`${config.apiPrefix}logs/${node.title}?path=${node.parent || ''}`)
`${config.apiPrefix}logs/detail?file=${node.title}&path=${
node.parent || ''
}`,
)
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setValue(data); setValue(data);
@@ -68,13 +64,13 @@ const Log = () => {
}; };
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
setCurrentNode(node);
setSelect(value);
if (node.key === select || !value) { if (node.key === select || !value) {
return; return;
} }
setCurrentNode(node);
setSelect(value);
if (node.type === 'directory') { if (node.type === 'directory') {
setValue(intl.get('请选择日志文件')); setValue(intl.get('请选择日志文件'));
return; return;
@@ -120,11 +116,10 @@ const Log = () => {
<> <>
{intl.get('确认删除')} {intl.get('确认删除')}
<Text style={{ wordBreak: 'break-all' }} type="warning"> <Text style={{ wordBreak: 'break-all' }} type="warning">
{' '} {select}
{select}{' '}
</Text> </Text>
{intl.get('文件')} {intl.get('文件')}
{currentNode.type === 'directory' ? intl.get('夹下所日志') : ''} {currentNode.type === 'directory' ? intl.get('夹下所日志') : ''}
{intl.get(',删除后不可恢复')} {intl.get(',删除后不可恢复')}
</> </>
), ),
+1 -6
View File
@@ -67,11 +67,7 @@ const EditModal = ({
const getDetail = (node: any) => { const getDetail = (node: any) => {
request request
.get( .get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`)
`${config.apiPrefix}scripts/detail?file=${node.title}&path=${
node.parent || ''
}`,
)
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setValue(data); setValue(data);
@@ -242,7 +238,6 @@ const EditModal = ({
minimap: { enabled: false }, minimap: { enabled: false },
lineNumbersMinChars: 3, lineNumbersMinChars: 3,
glyphMargin: false, glyphMargin: false,
accessibilitySupport: 'off'
}} }}
onMount={(editor) => { onMount={(editor) => {
editorRef.current = editor; editorRef.current = editor;
+2 -2
View File
@@ -39,8 +39,8 @@ const EditScriptNameModal = ({
setLoading(true); setLoading(true);
const { path = '', filename: inputFilename, directory = '' } = values; const { path = '', filename: inputFilename, directory = '' } = values;
const formData = new FormData(); const formData = new FormData();
formData.append('file', file || ''); formData.append('file', file as any);
formData.append('filename', file?.name || inputFilename); formData.append('filename', inputFilename);
formData.append('path', path); formData.append('path', path);
formData.append('content', ''); formData.append('content', '');
formData.append('directory', directory); formData.append('directory', directory);
+9 -42
View File
@@ -84,9 +84,9 @@ const Script = () => {
const getDetail = (node: any) => { const getDetail = (node: any) => {
request request
.get( .get(
`${config.apiPrefix}scripts/detail?file=${encodeURIComponent( `${config.apiPrefix}scripts/${encodeURIComponent(node.title)}?path=${
node.title, node.parent || ''
)}&path=${node.parent || ''}`, }`,
) )
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
@@ -115,13 +115,13 @@ const Script = () => {
}; };
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
setSelect(node.key);
setCurrentNode(node);
if (node.key === select || !value) { if (node.key === select || !value) {
return; return;
} }
setSelect(node.key);
setCurrentNode(node);
if (node.type === 'directory') { if (node.type === 'directory') {
setValue(intl.get('请选择脚本文件')); setValue(intl.get('请选择脚本文件'));
return; return;
@@ -218,9 +218,8 @@ const Script = () => {
<> <>
{intl.get('确认保存文件')} {intl.get('确认保存文件')}
<Text style={{ wordBreak: 'break-all' }} type="warning"> <Text style={{ wordBreak: 'break-all' }} type="warning">
{' '}
{currentNode.title} {currentNode.title}
</Text> </Text>{' '}
{intl.get(',保存后不可恢复')} {intl.get(',保存后不可恢复')}
</> </>
), ),
@@ -259,8 +258,7 @@ const Script = () => {
<> <>
{intl.get('确认删除')} {intl.get('确认删除')}
<Text style={{ wordBreak: 'break-all' }} type="warning"> <Text style={{ wordBreak: 'break-all' }} type="warning">
{' '} {select}
{select}{' '}
</Text> </Text>
{intl.get('文件')} {intl.get('文件')}
{currentNode.type === 'directory' ? intl.get('夹及其子文件') : ''} {currentNode.type === 'directory' ? intl.get('夹及其子文件') : ''}
@@ -382,7 +380,7 @@ const Script = () => {
}, [treeDom.current, data]); }, [treeDom.current, data]);
useHotkeys( useHotkeys(
'mod+s', 'meta+s',
(e) => { (e) => {
if (isEditing) { if (isEditing) {
saveFile(); saveFile();
@@ -391,36 +389,6 @@ const Script = () => {
{ enableOnFormTags: ['textarea'], preventDefault: true }, { 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) => { const action = (key: string | number) => {
switch (key) { switch (key) {
case 'save': case 'save':
@@ -643,7 +611,6 @@ const Script = () => {
fontSize: 12, fontSize: 12,
lineNumbersMinChars: 3, lineNumbersMinChars: 3,
glyphMargin: false, glyphMargin: false,
accessibilitySupport: 'off'
}} }}
onMount={(editor) => { onMount={(editor) => {
editorRef.current = editor; editorRef.current = editor;
+1 -6
View File
@@ -62,12 +62,7 @@ const RenameModal = ({
onCancel={() => handleCancel()} onCancel={() => handleCancel()}
confirmLoading={loading} confirmLoading={loading}
> >
<Form <Form form={form} layout="vertical" name="edit_name_modal">
form={form}
initialValues={{ name: currentNode?.title }}
layout="vertical"
name="edit_name_modal"
>
<Form.Item <Form.Item
name="name" name="name"
rules={[{ required: true, message: intl.get('请输入新名称') }]} rules={[{ required: true, message: intl.get('请输入新名称') }]}
+16 -37
View File
@@ -1,11 +1,10 @@
import { disableBody } from '@/utils'; import intl from 'react-intl-universal';
import config from '@/utils/config'; import React, { useEffect, useState, useRef, useCallback } from 'react';
import { Statistic, Modal, Tag, Button, Spin, message } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config';
import WebSocketManager from '@/utils/websocket'; import WebSocketManager from '@/utils/websocket';
import Ansi from 'ansi-to-react'; import Ansi from 'ansi-to-react';
import { Button, Modal, Statistic, message } from 'antd';
import { useCallback, useEffect, useRef, useState } from 'react';
import intl from 'react-intl-universal';
const { Countdown } = Statistic; const { Countdown } = Statistic;
@@ -78,11 +77,7 @@ const CheckUpdate = ({ systemInfo }: any) => {
</div> </div>
</> </>
), ),
content: ( content: <pre><Ansi>{lastLog}</Ansi></pre>,
<pre>
<Ansi>{lastLog}</Ansi>
</pre>
),
okText: intl.get('下载更新'), okText: intl.get('下载更新'),
cancelText: intl.get('以后再说'), cancelText: intl.get('以后再说'),
onOk() { onOk() {
@@ -107,17 +102,21 @@ const CheckUpdate = ({ systemInfo }: any) => {
okButtonProps: { disabled: true }, okButtonProps: { disabled: true },
title: intl.get('下载更新中...'), title: intl.get('下载更新中...'),
centered: true, centered: true,
content: ( content: <pre><Ansi>{value}</Ansi></pre>,
<pre>
<Ansi>{value}</Ansi>
</pre>
),
}); });
}; };
const reloadSystem = (type?: string) => { const showReloadModal = () => {
Modal.confirm({
width: 600,
maskClosable: false,
title: intl.get('确认重启'),
centered: true,
content: intl.get('系统安装包下载成功,确认重启'),
okText: intl.get('重启'),
onOk() {
request request
.put(`${config.apiPrefix}update/${type}`) .put(`${config.apiPrefix}system/reload`, { type: 'system' })
.then((_data: any) => { .then((_data: any) => {
message.success({ message.success({
content: ( content: (
@@ -133,7 +132,6 @@ const CheckUpdate = ({ systemInfo }: any) => {
), ),
duration: 30, duration: 30,
}); });
disableBody();
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
}, 30000); }, 30000);
@@ -141,18 +139,6 @@ const CheckUpdate = ({ systemInfo }: any) => {
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
}); });
};
const showReloadModal = () => {
Modal.confirm({
width: 600,
maskClosable: false,
title: intl.get('确认重启'),
centered: true,
content: intl.get('系统安装包下载成功,确认重启'),
okText: intl.get('重启'),
onOk() {
reloadSystem('system');
}, },
onCancel() { onCancel() {
modalRef.current.update({ modalRef.current.update({
@@ -220,13 +206,6 @@ const CheckUpdate = ({ systemInfo }: any) => {
<Button type="primary" onClick={checkUpgrade}> <Button type="primary" onClick={checkUpgrade}>
{intl.get('检查更新')} {intl.get('检查更新')}
</Button> </Button>
<Button
type="primary"
onClick={() => reloadSystem('reload')}
style={{ marginLeft: 8 }}
>
{intl.get('重新启动')}
</Button>
</> </>
); );
}; };
+13 -27
View File
@@ -105,15 +105,11 @@ const Dependence = () => {
label={intl.get('代理')} label={intl.get('代理')}
name="proxy" name="proxy"
extra={intl.get('代理与镜像源二选一即可')} extra={intl.get('代理与镜像源二选一即可')}
tooltip={{
title: intl.get('代理地址, 支持HTTP(S)/SOCK5'),
placement: 'topLeft',
}}
> >
<Input.Group compact> <Input.Group compact>
<Input <Input
placeholder={'http://1.1.1.1:8080'} placeholder={intl.get('代理地址, 支持HTTP(S)/SOCK5')}
style={{ width: 250 }} style={{ width: 360 }}
value={systemConfig?.dependenceProxy} value={systemConfig?.dependenceProxy}
onChange={(e) => { onChange={(e) => {
setSystemConfig({ setSystemConfig({
@@ -134,15 +130,11 @@ const Dependence = () => {
</Button> </Button>
</Input.Group> </Input.Group>
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={intl.get('Node 软件包镜像源')} name="node">
label={intl.get('Node 软件包镜像源')}
name="node"
tooltip={intl.get('NPM 镜像源')}
>
<Input.Group compact> <Input.Group compact>
<Input <Input
style={{ width: 250 }} style={{ width: 360 }}
placeholder={'https://registry.npmmirror.com'} placeholder={intl.get('NPM 镜像源')}
value={systemConfig?.nodeMirror} value={systemConfig?.nodeMirror}
onChange={(e) => { onChange={(e) => {
setSystemConfig({ setSystemConfig({
@@ -163,15 +155,11 @@ const Dependence = () => {
</Button> </Button>
</Input.Group> </Input.Group>
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={intl.get('Python 软件包镜像源')} name="python">
label={intl.get('Python 软件包镜像源')}
name="python"
tooltip={intl.get('PyPI 镜像源')}
>
<Input.Group compact> <Input.Group compact>
<Input <Input
style={{ width: 250 }} style={{ width: 360 }}
placeholder={'https://pypi.doubanio.com/simple/'} placeholder={intl.get('PyPI 镜像源')}
value={systemConfig?.pythonMirror} value={systemConfig?.pythonMirror}
onChange={(e) => { onChange={(e) => {
setSystemConfig({ setSystemConfig({
@@ -192,15 +180,13 @@ const Dependence = () => {
</Button> </Button>
</Input.Group> </Input.Group>
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={intl.get('Linux 软件包镜像源')} name="linux">
label={intl.get('Linux 软件包镜像源')}
name="linux"
tooltip={intl.get('alpine linux 镜像源')}
>
<Input.Group compact> <Input.Group compact>
<Input <Input
style={{ width: 250 }} style={{ width: 360 }}
placeholder={'https://mirrors.aliyun.com'} placeholder={intl.get(
'alpine linux 镜像源, 例如 https://mirrors.aliyun.com',
)}
value={systemConfig?.linuxMirror} value={systemConfig?.linuxMirror}
onChange={(e) => { onChange={(e) => {
setSystemConfig({ setSystemConfig({
+10 -17
View File
@@ -75,19 +75,11 @@ const Setting = () => {
title: intl.get('权限'), title: intl.get('权限'),
dataIndex: 'scopes', dataIndex: 'scopes',
key: 'scopes', key: 'scopes',
width: 500, width: '40%',
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return record.scopes.map((scope: any) => {
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}> return <Tag key={scope}>{(config.scopesMap as any)[scope]}</Tag>;
{record.scopes.map((scope: any) => { });
return (
<Tag style={{ marginRight: 0 }} key={scope}>
{(config.scopesMap as any)[scope]}
</Tag>
);
})}
</div>
);
}, },
}, },
{ {
@@ -356,9 +348,7 @@ const Setting = () => {
{ {
key: 'syslog', key: 'syslog',
label: intl.get('系统日志'), label: intl.get('系统日志'),
children: ( children: <SystemLog data={systemLogData} height={height} theme={theme}/>,
<SystemLog data={systemLogData} height={height} theme={theme} />
),
}, },
{ {
key: 'login', key: 'login',
@@ -368,13 +358,16 @@ const Setting = () => {
{ {
key: 'dependence', key: 'dependence',
label: intl.get('依赖设置'), label: intl.get('依赖设置'),
children: <Dependence />, children: <Dependence />
}, },
{ {
key: 'other', key: 'other',
label: intl.get('其他设置'), label: intl.get('其他设置'),
children: ( children: (
<Other reloadTheme={reloadTheme} systemInfo={systemInfo} /> <Other
reloadTheme={reloadTheme}
systemInfo={systemInfo}
/>
), ),
}, },
{ {
+5 -13
View File
@@ -22,7 +22,6 @@ import { UploadOutlined } from '@ant-design/icons';
import Countdown from 'antd/lib/statistic/Countdown'; import Countdown from 'antd/lib/statistic/Countdown';
import useProgress from './progress'; import useProgress from './progress';
import pick from 'lodash/pick'; import pick from 'lodash/pick';
import { disableBody } from '@/utils';
const dataMap = { const dataMap = {
'log-remove-frequency': 'logRemoveFrequency', 'log-remove-frequency': 'logRemoveFrequency',
@@ -142,7 +141,7 @@ const Other = ({
okText: intl.get('重启'), okText: intl.get('重启'),
onOk() { onOk() {
request request
.put(`${config.apiPrefix}update/data`) .put(`${config.apiPrefix}system/reload`, { type: 'data' })
.then(() => { .then(() => {
message.success({ message.success({
content: ( content: (
@@ -158,7 +157,6 @@ const Other = ({
), ),
duration: 30, duration: 30,
}); });
disableBody();
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
}, 30000); }, 30000);
@@ -273,18 +271,12 @@ const Other = ({
showUploadList={false} showUploadList={false}
maxCount={1} maxCount={1}
action={`${config.apiPrefix}system/data/import`} action={`${config.apiPrefix}system/data/import`}
onChange={({ file, event }) => { onChange={(e) => {
if (event?.percent) { if (e.event?.percent) {
showUploadProgress( showUploadProgress(parseFloat(e.event?.percent.toFixed(1)));
Math.min(parseFloat(event?.percent.toFixed(1)), 99), if (e.event?.percent === 100) {
);
}
if (file.status === 'done') {
showUploadProgress(100);
showReloadModal(); showReloadModal();
} }
if (file.status === 'error') {
message.error('上传失败');
} }
}} }}
name="data" name="data"
+5 -17
View File
@@ -2,6 +2,9 @@ import intl from 'react-intl-universal';
import { Modal, Progress } from 'antd'; import { Modal, Progress } from 'antd';
import { useRef } from 'react'; import { useRef } from 'react';
export default function useProgress(title: string) {
const modalRef = useRef<ReturnType<typeof Modal.info>>();
const ProgressElement = ({ percent }: { percent: number }) => ( const ProgressElement = ({ percent }: { percent: number }) => (
<Progress <Progress
style={{ display: 'flex', justifyContent: 'center' }} style={{ display: 'flex', justifyContent: 'center' }}
@@ -10,34 +13,19 @@ const ProgressElement = ({ percent }: { percent: number }) => (
/> />
); );
export default function useProgress(title: string) {
const modalRef = useRef<ReturnType<typeof Modal.info> | null>();
const showProgress = (percent: number) => { const showProgress = (percent: number) => {
if (modalRef.current) { if (modalRef.current) {
modalRef.current.update({ modalRef.current.update({
title: `${title}${ title: `${title}${percent >= 100 ? intl.get('成功') : intl.get('中...')}`,
percent >= 100 ? intl.get('成功') : intl.get('中...')
}`,
content: <ProgressElement percent={percent} />, content: <ProgressElement percent={percent} />,
okButtonProps: { disabled: percent !== 100 },
}); });
if (percent === 100) {
setTimeout(() => {
modalRef.current?.destroy();
modalRef.current = null;
});
}
} else { } else {
modalRef.current = Modal.info({ modalRef.current = Modal.info({
width: 600, width: 600,
maskClosable: false, maskClosable: false,
title: `${title}${ title: `${title}${percent >= 100 ? intl.get('成功') : intl.get('中...')}`,
percent >= 100 ? intl.get('成功') : intl.get('中...')
}`,
centered: true, centered: true,
content: <ProgressElement percent={percent} />, content: <ProgressElement percent={percent} />,
okButtonProps: { disabled: true },
}); });
} }
}; };
-31
View File
@@ -95,7 +95,6 @@ export default {
{ value: 'aibotk', label: intl.get('智能微秘书') }, { value: 'aibotk', label: intl.get('智能微秘书') },
{ value: 'iGot', label: 'IGot' }, { value: 'iGot', label: 'IGot' },
{ value: 'pushPlus', label: 'PushPlus' }, { value: 'pushPlus', label: 'PushPlus' },
{ value: 'wePlusBot', label: intl.get('微加机器人') },
{ value: 'chat', label: intl.get('群晖chat') }, { value: 'chat', label: intl.get('群晖chat') },
{ value: 'email', label: intl.get('邮箱') }, { value: 'email', label: intl.get('邮箱') },
{ value: 'lark', label: intl.get('飞书机器人') }, { value: 'lark', label: intl.get('飞书机器人') },
@@ -191,10 +190,6 @@ export default {
label: 'barkUrl', label: 'barkUrl',
tip: intl.get('BARK推送消息的跳转URL'), tip: intl.get('BARK推送消息的跳转URL'),
}, },
{
label: 'barkArchive',
tip: intl.get('BARK是否保存推送消息'),
},
], ],
telegramBot: [ telegramBot: [
{ {
@@ -313,27 +308,6 @@ export default {
), ),
}, },
], ],
wePlusBot: [
{
label: 'wePlusBotToken',
tip: intl.get(
'用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 https://www.weplusbot.com/',
),
required: true,
},
{
label: 'wePlusBotReceiver',
tip: intl.get(
'消息接收人',
),
},
{
label: 'wePlusBotVersion',
tip: intl.get(
'调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版',
),
},
],
lark: [ lark: [
{ {
label: 'larkKey', label: 'larkKey',
@@ -360,11 +334,6 @@ export default {
tip: intl.get('PushMe的Keyhttps://push.i-i.me/'), tip: intl.get('PushMe的Keyhttps://push.i-i.me/'),
required: true, required: true,
}, },
{
label: 'pushMeUrl',
tip: intl.get('自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口'),
required: false,
},
], ],
chronocat: [ chronocat: [
{ {
+5 -27
View File
@@ -340,18 +340,15 @@ export function parseCrontab(schedule: string): Date | null {
return null; return null;
} }
export function getCrontabsNextDate( export function getCrontabsNextDate(schedule: string, extra_schedules: string[]): Date | null {
schedule: string, let date = parseCrontab(schedule)
extra_schedules: string[],
): Date | null {
let date = parseCrontab(schedule);
if (extra_schedules?.length) { if (extra_schedules?.length) {
extra_schedules.forEach((x) => { extra_schedules.forEach(x => {
const _date = parseCrontab(x); const _date = parseCrontab(x)
if (_date && (!date || _date < date)) { if (_date && (!date || _date < date)) {
date = _date; date = _date;
} }
}); })
} }
return date; return date;
} }
@@ -366,22 +363,3 @@ export function getEditorMode(filename: string) {
const extension = getExtension(filename) as keyof typeof LANG_MAP; const extension = getExtension(filename) as keyof typeof LANG_MAP;
return LANG_MAP[extension]; return LANG_MAP[extension];
} }
export function disableBody() {
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.top = '0px';
overlay.style.left = '0px';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'transparent';
overlay.style.zIndex = '9999';
document.body.appendChild(overlay);
overlay.addEventListener('click', function (event) {
event.stopPropagation();
event.preventDefault();
});
document.body.style.overflow = 'hidden';
}
+27 -8
View File
@@ -1,9 +1,28 @@
version: 2.17.5 version: 2.17.0
changeLogLink: https://t.me/jiao_long/406 changeLogLink: https://t.me/jiao_long/401
publishTime: 2024-05-25 19:00 publishTime: 2024-01-17 23:00
changeLog: | changeLog: |
1. 修复有可能手动运行任务无日志 1. 系统设置增加依赖代理和镜像源设置
2. 重构 JavaScript 脚本通知文件 2. 更新 python 版本为 3.11
3. PushMe 通知支持自建服务 3. webhook 通知 body 增加 text/plain 类型,修复多定时任务服务重启未初始化
4. 增加微加机器人消息通道 4. 重新安装依赖时不检查是否已安装
5. 修复任务详情查看脚本错误 5. 配置管理、脚本管理增加双击目录进入编辑,ctrl/cmd + s 保存文件,感谢 https://github.com/luckrnx09
6. 系统设置增加依赖代理和镜像设置
7. 通知参数 TG_API_HOST 改为以 http 开头
8. 修改对比工具数据源,修复编辑器语言
9. 修改获取登录 ip 和 ip 地址方式
10. 修复备份数据 60 秒中断
11. 移除 config.sh 中 PipMirror 和 NpmMirror 配置
12. 修复健康检查命令
13. JavaScript 通知文件 tunnel 替换为 hpagent,移除 scripts 目录下 package.json
14. 日志和脚本增加文件大小展示,修改脚本管理列表排序
15. 修复 notify.js smtp 通知
16. 修改 schedule 服务代理环境变量
17. 修复任务详情脚本行数展示
18. 修复自定义通知 body 中 key 被自动转为小写
19. 修复初始化设置通知 shell 映射,邮箱通知错误提示
20. 修复邮件服务支持列表链接失效,感谢 https://github.com/Tunglies
21. 修复 chronocat 通知成功判断,感谢 https://github.com/youfak
22. 修复 chronocat 调用就结束执行的问题,感谢 https://github.com/6dylan6
23. 移除随机延迟相关配置及 ProxyUrl 默认值,修复拉库通知可能失败
24. 其他 bug 修复