Compare commits

...

30 Commits

Author SHA1 Message Date
whyour 86e3d8736b 更新版本 v2.15.15 2023-05-28 20:06:15 +08:00
whyour 85d32d00e1 修改打包 latest 镜像分支 2023-05-28 20:06:05 +08:00
whyour 6a52e2f804 修复 env.js 路径 2023-05-27 23:05:46 +08:00
whyour b4cb72c5d0 修改错误页面提示 2023-05-27 11:35:30 +08:00
whyour 79fd4ec58f 修复 nginx 变量替换 2023-05-26 17:08:40 +08:00
jzksnsjswkw 49ed5c6632 修复 可执行文件 提示找不到文件的问题 (#1943) 2023-05-26 15:17:47 +08:00
whyour ffba7ee4a1 增加定时任务服务启动失败日志 2023-05-25 21:45:28 +08:00
whyour 91d080472f 修复环境变量接口前缀 2023-05-24 23:46:07 +08:00
whyour 826f214f0f 修复设置通知错误时提示不清晰 2023-05-24 23:37:44 +08:00
whyour ca150dc6a6 修复 docker-compose 配置 2023-05-22 20:20:46 +08:00
whyour ae5db60211 修复筛选可能报错 2023-05-21 12:29:29 +08:00
whyour 490bdc15f6 支持非根目录部署 2023-05-19 01:10:33 +08:00
whyour 1377fe31aa 更新版本 v2.15.14 2023-05-15 23:44:42 +08:00
whyour 3d1cf6bf2e 修改群晖名称 2023-05-15 23:44:32 +08:00
whyour 6ac755b478 修复后端 html 缓存 2023-05-15 22:34:06 +08:00
whyour e70fa83074 修复获取配置文件参数 2023-05-14 20:53:45 +08:00
whyour 41521f1d08 修复 token 任务脚本路径,容器启动 bot 2023-05-13 01:10:49 +08:00
whyour 8b7bef097c 修复拉取仓库逻辑 2023-05-12 18:28:01 +08:00
whyour 618756d4a9 修复设置代理时,服务启动失败 2023-05-10 23:17:05 +08:00
whyour 70d0c71414 修复错误日志样式 2023-05-10 23:01:51 +08:00
whyour 65eb9c806d 更新 sqlite 2023-05-10 21:58:11 +08:00
whyour 50718ba714 修复 public 服务转发 2023-05-10 01:07:46 +08:00
whyour 7c3bb90d12 修复删除依赖和移动环境变量接口数据结构 2023-05-08 21:29:25 +08:00
whyour f66390eabd 修改初始系统状态逻辑已经错误页面 2023-05-07 23:40:37 +08:00
whyour 481646501c 修复订阅更新仓库失败 2023-05-06 22:58:05 +08:00
whyour 5baa8e4391 增加 pm2 平滑重启 2023-05-06 00:09:03 +08:00
whyour 3d11210aeb pnpm 版本降至 8.3.1 2023-05-05 00:26:23 +08:00
whyour e1863407f2 修复 npm 源判断 2023-05-04 22:23:55 +08:00
whyour 0c28b63ab7 修改容器启动流程 2023-05-04 22:07:39 +08:00
whyour 8b8bd279c6 修复 nginx 转发stream接口失效,停止运行命令支持 pid 参数 2023-05-04 21:28:14 +08:00
47 changed files with 3472 additions and 2952 deletions
+6 -5
View File
@@ -45,7 +45,7 @@ jobs:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: latest
version: '8.3.1'
- uses: actions/setup-node@v3
with:
@@ -85,8 +85,6 @@ jobs:
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH}
build:
if: github.ref != 'refs/heads/master'
needs: build-static
runs-on: ubuntu-latest
@@ -99,7 +97,7 @@ jobs:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: latest
version: '8.3.1'
- uses: actions/setup-node@v3
with:
cache: 'pnpm'
@@ -131,11 +129,14 @@ jobs:
ghcr.io/${{ github.repository }}
# generate Docker tags based on the following events/attributes
# nightly, master, pr-2, 1.2.3, 1.2, 1
flavor: |
latest=false
tags: |
type=schedule,pattern=nightly
type=edge
type=ref,event=branch
type=ref,event=pr
type=ref,event=branch,enable=${{ github.ref != format('refs/heads/{0}', 'master') }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
+7 -4
View File
@@ -1,26 +1,28 @@
import { defineConfig } from '@umijs/max';
const CompressionPlugin = require('compression-webpack-plugin');
const baseUrl = process.env.QlBaseUrl || '/';
export default defineConfig({
hash: true,
antd: {},
outputPath: 'static/dist',
fastRefresh: true,
favicons: ['./images/favicon.svg'],
favicons: [`https://qn.whyour.cn/favicon.svg`],
mfsu: {
strategy: 'eager',
},
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
proxy: {
'/api/public': {
[`${baseUrl}api/public`]: {
target: 'http://127.0.0.1:5400/',
changeOrigin: true,
pathRewrite: { '^/api/public': '/api/' },
pathRewrite: { [`^${baseUrl}api/public`]: '/api' },
},
'/api': {
[`${baseUrl}api`]: {
target: 'http://127.0.0.1:5600/',
changeOrigin: true,
ws: true,
pathRewrite: { [`^${baseUrl}api`]: '/api' },
},
},
chainWebpack: ((config: any) => {
@@ -38,6 +40,7 @@ export default defineConfig({
'react-dom': 'window.ReactDOM',
},
headScripts: [
`./api/env.js`,
'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
],
+1 -1
View File
@@ -198,7 +198,7 @@ $ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
# Recommended use pnpm https://pnpm.io/zh/installation
$ npm install -g pnpm
$ npm install -g pnpm@8.3.1
$ pnpm install
$ pnpm start
```
+5 -1
View File
@@ -59,6 +59,8 @@ podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
-e QlBaseUrl="/" \
--name qinglong \
--hostname qinglong \
docker.io/whyour/qinglong:latest
@@ -97,6 +99,8 @@ systemctl restart docker
docker run -dit \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
-e QlBaseUrl="/" \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
@@ -197,7 +201,7 @@ $ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
# 推荐使用 pnpm https://pnpm.io/zh/installation
$ npm install -g pnpm
$ npm install -g pnpm@8.3.1
$ pnpm install
$ pnpm start
```
+4 -3
View File
@@ -5,6 +5,7 @@ import { Logger } from 'winston';
import config from '../config';
import * as fs from 'fs';
import { celebrate, Joi } from 'celebrate';
import { join } from 'path';
const route = Router();
export default (app: Router) => {
@@ -41,11 +42,11 @@ export default (app: Router) => {
}
if (req.params.file.includes('sample')) {
content = getFileContentByName(
`${config.samplePath}${req.params.file}`,
join(config.samplePath, req.params.file),
);
} else {
content = getFileContentByName(
`${config.configPath}${req.params.file}`,
join(config.configPath, req.params.file),
);
}
res.send({ code: 200, data: content });
@@ -70,7 +71,7 @@ export default (app: Router) => {
if (config.blackFileList.includes(name)) {
res.send({ code: 403, message: '文件无法访问' });
}
const path = `${config.configPath}${name}`;
const path = join(config.configPath, name);
fs.writeFileSync(path, content);
res.send({ code: 200, message: '保存成功' });
} catch (e) {
+3 -3
View File
@@ -94,7 +94,7 @@ export default (app: Router) => {
path += '/';
}
if (!path.startsWith('/')) {
path = `${config.scriptPath}${path}`;
path = join(config.scriptPath, path);
}
if (config.writePathList.every((x) => !path.startsWith(x))) {
return res.send({
@@ -124,7 +124,7 @@ export default (app: Router) => {
if (fs.existsSync(originFilePath)) {
fs.copyFileSync(
originFilePath,
`${config.bakPath}${originFilename.replace(/\//g, '')}`,
join(config.bakPath, originFilename.replace(/\//g, '')),
);
if (filename !== originFilename) {
fs.unlinkSync(originFilePath);
@@ -207,7 +207,7 @@ export default (app: Router) => {
let { filename } = req.body as {
filename: string;
};
const filePath = `${config.scriptPath}${filename}`;
const filePath = join(config.scriptPath, filename);
// const stats = fs.statSync(filePath);
// res.set({
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
+5 -1
View File
@@ -170,6 +170,9 @@ export default (app: Router) => {
await systemService.run(
{ ...req.body, logPath },
{
onStart: async (cp, startTime) => {
res.setHeader('QL-Task-Pid', `${cp.pid}`);
},
onEnd: async (cp, endTime, diff) => {
res.end();
},
@@ -195,7 +198,8 @@ export default (app: Router) => {
'/command-stop',
celebrate({
body: Joi.object({
command: Joi.string().required(),
command: Joi.string().optional(),
pid: Joi.number().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
+1
View File
@@ -18,6 +18,7 @@ async function startServer() {
const server = app
.listen(config.port, () => {
Logger.debug(`✌️ 后端服务启动成功!`);
process.send?.('ready');
})
.on('error', (err) => {
Logger.error(err);
+5
View File
@@ -92,6 +92,11 @@ export default {
'/api/system',
'/api/user/init',
'/api/user/notification/init',
'/open/user/login',
'/open/user/two-factor/login',
'/open/system',
'/open/user/init',
'/open/user/notification/init',
],
versionFile,
lastVersionFile,
+20
View File
@@ -0,0 +1,20 @@
import { Request, Response } from 'express';
import { pick } from 'lodash';
let pickedEnv: Record<string, string>;
function getPickedEnv() {
if (pickedEnv) return pickedEnv;
const picked = pick(process.env, ['QlBaseUrl']);
pickedEnv = picked as Record<string, string>;
return picked;
}
export function serveEnv(_req: Request, res: Response) {
res.type('.js');
res.send(
Object.entries(getPickedEnv())
.map(([k, v]) => `window.__ENV__${k}=${JSON.stringify(v)};`)
.join('\n'),
);
}
+2 -1
View File
@@ -1,9 +1,10 @@
import { Sequelize, Transaction } from 'sequelize';
import config from '../config/index';
import { join } from 'path';
export const sequelize = new Sequelize({
dialect: 'sqlite',
storage: `${config.dbPath}database.sqlite`,
storage: join(config.dbPath, 'database.sqlite'),
logging: false,
retry: {
max: 10,
+13 -10
View File
@@ -14,22 +14,25 @@ import handler from 'serve-handler';
import * as Sentry from '@sentry/node';
import { EnvModel } from '../data/env';
import { errors } from 'celebrate';
import path from 'path';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { serveEnv } from '../config/serverEnv';
export default ({ app }: { app: Application }) => {
app.enable('trust proxy');
app.use(cors());
app.get(`${config.api.prefix}/env.js`, serveEnv);
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
app.use((req, res, next) => {
if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
next();
} else {
return handler(req, res, {
public: 'static/dist',
rewrites: [{ source: '**', destination: '/index.html' }],
});
}
});
app.use(
'/api/public',
createProxyMiddleware({
target: `http://localhost:${config.publicPort}/api`,
changeOrigin: true,
pathRewrite: { '/api/public': '' },
}),
);
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
+4 -2
View File
@@ -4,6 +4,7 @@ import ScheduleService from '../services/schedule';
import SubscriptionService from '../services/subscription';
import config from '../config';
import { fileExist } from '../config/util';
import { join } from 'path';
export default async () => {
const systemService = Container.get(SystemService);
@@ -11,8 +12,9 @@ export default async () => {
const subscriptionService = Container.get(SubscriptionService);
// 生成内置token
let tokenCommand = `tsx ${config.rootPath}/back/token.ts`;
const tokenFile = `${config.rootPath}static/build/token.js`;
let tokenCommand = `tsx ${join(config.rootPath, 'back/token.ts')}`;
const tokenFile = join(config.rootPath, 'static/build/token.js');
if (await fileExist(tokenFile)) {
tokenCommand = `node ${tokenFile}`;
}
+8 -2
View File
@@ -9,8 +9,14 @@ export default async ({ server }: { server: Server }) => {
process.on('SIGINT', () => {
Logger.info('✌️ Server need close');
server.close(() => {
Logger.info('✌️ Server closed');
process.exit(0);
setTimeout(() => {
process.exit();
}, 10000);
});
setTimeout(() => {
console.log('Forcing server close !!!');
process.exit(1);
}, 15000);
});
};
+3 -3
View File
@@ -1,5 +1,4 @@
import express from 'express';
import { exec } from 'child_process';
import Logger from './loaders/logger';
import config from './config';
import { HealthClient } from './protos/health';
@@ -14,9 +13,9 @@ const client = new HealthClient(
app.get('/api/health', (req, res) => {
client.check({ service: 'cron' }, (err, response) => {
if (err) {
return res.status(500).send({ error: err });
return res.status(500).send({ code: 500, error: err });
}
return res.status(200).send({ data: response });
return res.status(200).send({ code: 200, data: response });
});
});
@@ -26,6 +25,7 @@ app
await require('./loaders/db').default();
Logger.debug(`✌️ 公共服务启动成功!`);
process.send?.('ready');
})
.on('error', (err) => {
Logger.error(err);
+5 -2
View File
@@ -17,10 +17,13 @@ const check = async (
if (res.includes('200')) {
return callback(null, { status: 1 });
}
const errLog = await promiseExec(
const panelErrLog = await promiseExec(
`tail -n 300 ~/.pm2/logs/panel-error.log`,
);
return callback(new Error(errLog));
const scheduleErrLog = await promiseExec(
`tail -n 300 ~/.pm2/logs/schedule-error.log`,
);
return callback(new Error(`${scheduleErrLog}\n${panelErrLog}`));
default:
return callback(null, { status: 1 });
+5 -1
View File
@@ -13,8 +13,12 @@ server.addService(CronService, { addCron, delCron });
server.bindAsync(
`localhost:${config.cronPort}`,
ServerCredentials.createInsecure(),
() => {
(err, port) => {
if (err) {
throw err;
}
server.start();
Logger.debug(`✌️ 定时服务启动成功!`);
process.send?.('ready');
},
);
+83 -19
View File
@@ -93,7 +93,11 @@ export default class NotificationService {
},
})
.json();
return typeof res.id === 'number';
if (typeof res.id === 'number') {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
@@ -109,7 +113,11 @@ export default class NotificationService {
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
})
.json();
return res.retcode === 0;
if (res.retcode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
@@ -128,7 +136,11 @@ export default class NotificationService {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.errno === 0 || res.data.errno === 0;
if (res.errno === 0 || res.data.errno === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
@@ -147,9 +159,14 @@ export default class NotificationService {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return (
res.content.result.length !== undefined && res.content.result.length > 0
);
if (
res.content.result.length !== undefined &&
res.content.result.length > 0
) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
@@ -166,7 +183,11 @@ export default class NotificationService {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.success;
if (res.success) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
@@ -190,7 +211,11 @@ export default class NotificationService {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.code === 200;
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);
}
@@ -234,7 +259,11 @@ export default class NotificationService {
agent,
})
.json();
return !!res.ok;
if (res.ok) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
@@ -263,7 +292,11 @@ export default class NotificationService {
},
})
.json();
return res.errcode === 0;
if (res.errcode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
@@ -284,7 +317,11 @@ export default class NotificationService {
},
})
.json();
return res.errcode === 0;
if (res.errcode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
@@ -359,7 +396,11 @@ export default class NotificationService {
)
.json();
return res.errcode === 0;
if (res.errcode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
@@ -403,8 +444,11 @@ export default class NotificationService {
},
})
.json();
return res.code === 0;
if (res.code === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
@@ -422,7 +466,11 @@ export default class NotificationService {
})
.json();
return res.ret === 0;
if (res.ret === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
@@ -444,7 +492,11 @@ export default class NotificationService {
})
.json();
return res.code === 200;
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);
}
@@ -468,7 +520,11 @@ export default class NotificationService {
headers: { 'Content-Type': 'application/json' },
})
.json();
return res.StatusCode === 0;
if (res.StatusCode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
@@ -495,7 +551,11 @@ export default class NotificationService {
transporter.close();
return !!info.messageId;
if (info.messageId) {
return true;
} else {
throw new Error(JSON.stringify(info));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
@@ -529,7 +589,11 @@ export default class NotificationService {
};
try {
const res = await got(formatUrl, options);
return String(res.statusCode).startsWith('20');
if (String(res.statusCode).startsWith('20')) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
+3 -1
View File
@@ -60,7 +60,9 @@ export default class SshKeyService {
if (host === 'github.com') {
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`;
}
const proxyStr = proxy ? ` ProxyCommand nc -v -x ${proxy} %h %p\n` : '';
const proxyStr = proxy
? ` ProxyCommand nc -v -x ${proxy} %h %p 2>/dev/null\n`
: '';
const config = `Host ${alias}\n Hostname ${host}\n IdentityFile ${path.join(
this.sshPath,
alias,
+13 -4
View File
@@ -190,13 +190,22 @@ export default class SystemService {
);
}
public async stop({ command }: { command: string }) {
public async stop({ command, pid }: { command: string; pid: number }) {
if (!pid && !command) {
return { code: 400, message: '参数错误' };
}
if (pid) {
await killTask(pid);
return { code: 200 };
}
if (!command.startsWith(TASK_COMMAND)) {
command = `${TASK_COMMAND} ${command}`;
}
const pid = await getPid(command);
if (pid) {
await killTask(pid);
const _pid = await getPid(command);
if (_pid) {
await killTask(_pid);
return { code: 200 };
} else {
return { code: 400, message: '任务未找到' };
+20 -11
View File
@@ -1,6 +1,11 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { createRandomString, getNetIp, getPlatform } from '../config/util';
import {
createRandomString,
fileExist,
getNetIp,
getPlatform,
} from '../config/util';
import config from '../config';
import * as fs from 'fs';
import jwt from 'jsonwebtoken';
@@ -205,17 +210,16 @@ export default class UserService {
}
private initAuthInfo() {
const newPassword = createRandomString(16, 22);
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: 'admin',
password: newPassword,
password: 'admin',
}),
);
return {
code: 100,
message: '已初始化密码,请前往auth.json查看并重新登录',
message: '未找到认证文件,重新初始化',
};
}
@@ -240,13 +244,18 @@ export default class UserService {
return { code: 200, data: avatar, message: '更新成功' };
}
public getUserInfo(): Promise<any> {
return new Promise((resolve) => {
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
if (err) console.log(err);
resolve(JSON.parse(data));
});
});
public async getUserInfo(): Promise<any> {
const authFileExist = await fileExist(config.authConfigFile);
if (!authFileExist) {
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: 'admin',
password: 'admin',
}),
);
}
return this.getAuthInfo();
}
public initTwoFactor() {
+6 -5
View File
@@ -3,9 +3,9 @@ COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
RUN set -x \
&& apk update \
&& apk add nodejs npm git \
&& npm i -g pnpm \
&& npm i -g pnpm@8.3.1 \
&& cd /tmp/build \
&& pnpm install --prod
&& pnpm --registry https://registry.npmmirror.com install --prod
FROM python:3.10-alpine
@@ -29,7 +29,6 @@ RUN set -x \
&& apk upgrade \
&& apk --no-cache add -f bash \
coreutils \
moreutils \
git \
curl \
wget \
@@ -41,6 +40,7 @@ RUN set -x \
jq \
openssh \
procps \
netcat-openbsd \
npm \
&& rm -rf /var/cache/apk/* \
&& apk update \
@@ -49,7 +49,8 @@ RUN set -x \
&& git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \
&& git config --global http.postBuffer 524288000 \
&& npm install -g pnpm \
&& npm install -g pnpm@8.3.1 \
&& cd && pnpm config set registry https://registry.npmmirror.com \
&& pnpm add -g pm2 tsx \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
@@ -71,7 +72,7 @@ COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
WORKDIR ${QL_DIR}
HEALTHCHECK --interval=5s --timeout=2s --retries=10 \
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
CMD curl -sf http://127.0.0.1:5400/api/health || exit 1
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+6 -5
View File
@@ -1,14 +1,15 @@
version: '2'
services:
web:
# alpine 基础镜像版本
image: whyour/qinglong:latest
# debian-slim 基础镜像版本
# image: whyour/qinglong:debian
volumes:
- ./data:/ql/data
ports:
- "0.0.0.0:5700:5700"
environment:
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
QlBaseUrl: '/'
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-sf", "http://127.0.0.1:5400/api/health", "||", "exit", "1"]
interval: 2m
timeout: 10s
retries: 3
+14 -18
View File
@@ -10,12 +10,21 @@ echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d
make_dir /run/nginx
init_nginx
fix_config
pm2 l &>/dev/null
pm2 flush &>/dev/null
echo -e "======================2. 安装依赖========================\n"
patch_version
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
current_npm_registry=$(cd && pnpm config get registry)
is_equal_registry=$(echo $current_npm_registry | grep "${NpmMirror}")
if [[ "$is_equal_registry" == "" ]]; then
cd && pnpm config set registry $NpmMirror
pnpm install -g
fi
update_depend
echo
@@ -23,36 +32,23 @@ echo -e "======================3. 启动nginx========================\n"
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "nginx启动成功...\n"
echo -e "======================4. 启动面板监控========================\n"
pm2 delete public &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time
echo -e "监控服务启动成功...\n"
echo -e "======================5. 启动主服务========================\n"
pm2 delete panel &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time
echo -e "主服务启动成功...\n"
echo -e "======================6. 启动定时服务========================\n"
pm2 delete schedule &>/dev/null
pm2 start $dir_static/build/schedule/index.js -n schedule --source-map-support --time
echo -e "定时任务启动成功...\n"
echo -e "======================4. 启动pm2服务========================\n"
reload_pm2
if [[ $AutoStartBot == true ]]; then
echo -e "======================7. 启动bot========================\n"
echo -e "======================5. 启动bot========================\n"
nohup ql -l bot >$dir_log/bot.log 2>&1 &
echo -e "bot后台启动中...\n"
fi
if [[ $EnableExtraShell == true ]]; then
echo -e "======================8. 执行自定义脚本========================\n"
echo -e "====================6. 执行自定义脚本========================\n"
nohup ql -l extra >$dir_log/extra.log 2>&1 &
echo -e "自定义脚本后台执行中...\n"
fi
echo -e "############################################################\n"
echo -e "容器启动成功..."
echo -e "\n请先访问5700端口,登录成功面板之后再执行添加定时任务..."
echo -e "############################################################\n"
crond -f >/dev/null
+10 -6
View File
@@ -14,31 +14,33 @@ map $http_upgrade $connection_upgrade {
server {
listen 5700;
IPV6_CONFIG
root /ql/static/dist;
ssl_session_timeout 5m;
location QL_BASE_URL/api/public/ {
location QL_BASE_URLapi/public/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://publicApi/api/;
proxy_buffering off;
}
location QL_BASE_URL/api/ {
location QL_BASE_URLapi/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://baseApi/api/;
proxy_buffering off;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location QL_BASE_URL/open/ {
location QL_BASE_URLopen/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://baseApi/open/;
proxy_buffering off;
}
gzip on;
@@ -49,10 +51,12 @@ server {
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.0;
QL_ROOT_CONFIG
location QL_BASE_URL/ {
location QL_BASE_URL_LOCATION {
QL_ALIAS_CONFIG
index index.html index.htm;
try_files $uri $uri/ QL_BASE_URL/index.html;
try_files $uri QL_BASE_URLindex.html;
}
location ~ .*\.(html)$ {
+34
View File
@@ -0,0 +1,34 @@
module.exports = {
apps: [
{
name: 'schedule',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
source_map_support: true,
time: true,
script: 'static/build/schedule/index.js',
},
{
name: 'public',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
source_map_support: true,
time: true,
script: 'static/build/public.js',
},
{
name: 'panel',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
source_map_support: true,
time: true,
script: 'static/build/app.js',
},
],
};
+2 -1
View File
@@ -72,6 +72,7 @@
"form-data": "^4.0.0",
"got": "^11.8.2",
"hpagent": "^0.1.2",
"http-proxy-middleware": "^2.0.6",
"iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1",
@@ -86,7 +87,7 @@
"sequelize": "^6.25.5",
"serve-handler": "^6.1.3",
"sockjs": "^0.3.24",
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.2",
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
"toad-scheduler": "^1.6.0",
"typedi": "^0.10.0",
"uuid": "^8.3.2",
+3013 -2720
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.8 KiB

+1 -1
View File
@@ -2,7 +2,7 @@
create_token() {
local token_command="tsx ${dir_root}/back/token.ts"
local token_file="${dir_root}static/build/token.js"
local token_file="${dir_root}/static/build/token.js"
if [[ -f $token_file ]]; then
token_command="node ${token_file}"
fi
+12 -8
View File
@@ -74,19 +74,23 @@ check_pm2() {
fi
}
start_public() {
echo -e "=====> 启动公开服务\n"
pm2 delete public --source-map-support --time &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
}
main() {
echo -e "=====> 开始检测"
npm i -g pnpm
npm i -g pnpm@8.3.1
patch_version
apk add procps netcat-openbsd
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
cd && pnpm config set registry $NpmMirror
pnpm install -g
fi
pnpm add -g pm2 tsx
reset_env
start_public
copy_dep
check_ql
check_nginx
+1 -1
View File
@@ -193,7 +193,7 @@ run_else() {
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
file_param=${file_param/$relative_path\//.\/}
fi
shift
+44 -63
View File
@@ -18,6 +18,7 @@ dir_update_log=$dir_log/update
ql_static_repo=$dir_repo/static
## 文件
file_ecosystem_js=$dir_root/ecosystem.config.js
file_config_sample=$dir_sample/config.sample.sh
file_env=$dir_config/env.sh
file_sharecode=$dir_config/sharecode.sh
@@ -34,7 +35,6 @@ file_notify_js_sample=$dir_sample/notify.js
file_notify_py_sample=$dir_sample/notify.py
file_notify_py=$dir_scripts/notify.py
file_notify_js=$dir_scripts/sendNotify.js
task_error_log_path=$dir_log/task_error.log
nginx_app_conf=$dir_root/docker/front.conf
nginx_conf=$dir_root/docker/nginx.conf
dep_notify_py=$dir_dep/notify.py
@@ -67,7 +67,7 @@ import_config() {
[[ -f $file_config_user ]] && . $file_config_user
[[ -f $file_env ]] && . $file_env
ql_base_url=${QlBaseUrl:-""}
ql_base_url=${QlBaseUrl:-"/"}
command_timeout_time=${CommandTimeoutTime:-""}
proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"}
@@ -98,6 +98,9 @@ set_proxy() {
unset_proxy() {
unset http_proxy
unset https_proxy
unset ftp_proxy
unset all_proxy
unset no_proxy
}
make_dir() {
@@ -262,17 +265,6 @@ npm_install_sub() {
fi
}
npm_install_1() {
local dir_current=$(pwd)
local dir_work=$1
cd $dir_work
echo -e "运行 pnpm install...\n"
npm_install_sub
[[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 pnpm install...\n"
cd $dir_current
}
npm_install_2() {
local dir_current=$(pwd)
local dir_work=$1
@@ -280,10 +272,6 @@ npm_install_2() {
cd $dir_work
echo -e "安装 $dir_work 依赖包...\n"
npm_install_sub
if [[ $? -ne 0 ]]; then
echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n"
npm_install_1 $dir_work
fi
cd $dir_current
}
@@ -315,11 +303,13 @@ git_clone_scripts() {
echo -e "开始克隆仓库 $url$dir\n"
set_proxy "$proxy"
git clone --depth=1 $part_cmd $url $dir
exit_status=$?
unset_proxy
reset_branch "$branch"
reset_branch "$branch" "$dir"
unset_proxy
}
git_pull_scripts() {
@@ -329,19 +319,25 @@ git_pull_scripts() {
local proxy="$3"
cd $dir_work
echo -e "开始更新仓库:$dir_work"
set_proxy "$proxy"
if [[ ! $branch ]]; then
branch=$(cd $dir_work && git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)
fi
local pre_commit_id=$(git rev-parse --short HEAD)
set_proxy "$proxy"
git fetch --depth=1 --all
git pull --depth=1 &>/dev/null
exit_status=$?
unset_proxy
reset_branch "$branch" "$dir_work"
reset_branch "$branch"
git fetch --depth 1 origin $branch
exit_status=$?
reset_branch "$branch" "$dir_work"
local cur_commit_id=$(git rev-parse --short HEAD)
if [[ $cur_commit_id != $pre_commit_id ]]; then
exit_status=0
fi
unset_proxy
cd $dir_current
}
@@ -359,19 +355,16 @@ reset_romote_url() {
git init
git remote add origin $url &>/dev/null
fi
cd $dir_current
}
reset_branch() {
local branch="$1"
local part_cmd="HEAD"
if [[ $branch ]]; then
part_cmd="origin/${branch}"
git checkout -B "$branch" &>/dev/null
git branch --set-upstream-to=$part_cmd $branch &>/dev/null
fi
local part_cmd="origin/${branch}"
git remote set-branches origin $branch
git reset --hard $part_cmd &>/dev/null
git checkout -b $branch $part_cmd &>/dev/null
}
random_range() {
@@ -381,16 +374,11 @@ random_range() {
}
reload_pm2() {
pm2 l &>/dev/null
cd $dir_root
# 代理会影响 grpc 服务
unset_proxy
pm2 flush &>/dev/null
echo -e "启动面板服务\n"
pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
echo -e "启动定时服务\n"
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_static/build/schedule/index.js -n schedule --source-map-support --time &>/dev/null
pm2 startOrGracefulReload $file_ecosystem_js
}
diff_time() {
@@ -440,19 +428,6 @@ format_timestamp() {
}
patch_version() {
# 兼容pnpm@7
pnpm setup &>/dev/null
source ~/.bashrc
apk add procps
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
cd && pnpm config set registry $NpmMirror
pnpm install -g --force
fi
git config --global pull.rebase false
cp -f $dir_root/.env.example $dir_root/.env
@@ -492,8 +467,20 @@ patch_version() {
init_nginx() {
cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
local location_url="/"
local aliasStr=""
local rootStr=""
if [[ $ql_base_url != "/" ]]; then
location_url="^~${ql_base_url%*/}"
aliasStr="alias ${dir_static}/dist;"
else
rootStr="root ${dir_static}/dist;"
fi
sed -i "s,QL_ALIAS_CONFIG,${aliasStr},g" /etc/nginx/conf.d/front.conf
sed -i "s,QL_ROOT_CONFIG,${rootStr},g" /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL_LOCATION,${location_url},g" /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${ql_base_url},g" /etc/nginx/conf.d/front.conf
ipv6=$(ip a | grep inet6)
ipv6Str=""
if [[ $ipv6 ]]; then
@@ -509,11 +496,6 @@ handle_task_before() {
[[ $is_macos -eq 0 ]] && check_server
if [[ -s $task_error_log_path ]]; then
cat $task_error_log_path
echo -e "加载 config.sh 出错,请手动检查"
fi
. $file_task_before "$@"
}
@@ -536,6 +518,5 @@ init_env
detect_termux
detect_macos
define_cmd
fix_config
import_config $1 2>$task_error_log_path
import_config $1
+6 -9
View File
@@ -243,10 +243,9 @@ update_qinglong() {
echo -e "使用 ${mirror} 源更新...\n"
export isFirstStartServer=false
local all_branch=$(cd ${dir_root} && git branch -a)
local primary_branch="master"
if [[ "${all_branch}" =~ "${current_branch}" ]]; then
primary_branch="${current_branch}"
if [[ "${QL_BRANCH}" == "develop" ]]; then
primary_branch="develop"
fi
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch}
@@ -257,6 +256,9 @@ update_qinglong() {
cp -f $file_config_sample $dir_config/config.sample.sh
update_depend
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
update_qinglong_static "$1" "$primary_branch"
else
echo -e "\n更新青龙源文件失败,请检查网络...\n"
@@ -454,12 +456,6 @@ main() {
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
fi
if [[ -s $task_error_log_path ]]; then
eval cat $task_error_log_path $cmd
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
eval echo $cmd
fi
if [[ "$show_log" == "true" ]] && [[ $ID ]]; then
eval echo -e "请移除 -l 参数" $cmd
exit 1
@@ -467,6 +463,7 @@ main() {
case $p1 in
update)
fix_config
eval update_qinglong "$2" $cmd
;;
extra)
+16
View File
@@ -0,0 +1,16 @@
const baseUrl = window.__ENV__QlBaseUrl || '/';
export function modifyClientRenderOpts(memo: any) {
return {
...memo,
publicPath: baseUrl,
basename: baseUrl,
};
}
export function modifyContextOpts(memo: any) {
return {
...memo,
basename: baseUrl,
};
}
+8 -2
View File
@@ -332,8 +332,14 @@ select:-webkit-autofill:focus {
}
.ant-pro-sider-logo {
h1 {
margin-left: 5px !important;
.title {
height: 32px;
margin: 0 5px;
font-weight: 600;
font-size: 16px;
line-height: 32px;
vertical-align: middle;
animation: pro-layout-title-hide 0.3s;
}
img {
+23 -10
View File
@@ -98,8 +98,7 @@ export default function () {
})
.catch((error) => {
console.log(error);
})
.finally(() => setInitLoading(false));
});
};
const getUser = (needLoading = true) => {
@@ -120,6 +119,22 @@ export default function () {
});
};
const getHealthStatus = () => {
request
.get(`${config.apiPrefix}public/health`)
.then((res) => {
if (res?.data?.status === 1) {
getSystemInfo();
} else {
history.push('/error');
}
})
.catch((error) => {
history.push('/error');
})
.finally(() => setInitLoading(false));
};
const reloadUser = (needLoading = false) => {
getUser(needLoading);
};
@@ -131,10 +146,8 @@ export default function () {
}, [location.pathname]);
useEffect(() => {
if (!systemInfo) {
getSystemInfo();
}
}, [systemInfo]);
getHealthStatus();
}, []);
useEffect(() => {
if (theme === 'vs-dark') {
@@ -267,11 +280,10 @@ export default function () {
selectedKeys={[location.pathname]}
loading={loading}
ErrorBoundary={Sentry.ErrorBoundary}
logo={<Image preview={false} src="https://qn.whyour.cn/logo.png" />}
// @ts-ignore
title={
logo={
<>
<span style={{ fontSize: 16, marginRight: 5 }}></span>
<Image preview={false} src="https://qn.whyour.cn/logo.png" />
<span className="title"></span>
<a
href={systemInfo?.changeLogLink}
target="_blank"
@@ -300,6 +312,7 @@ export default function () {
</a>
</>
}
title={false}
menuItemRender={(menuItemProps: any, defaultDom: any) => {
if (
menuItemProps.isUrl ||
+2 -2
View File
@@ -253,9 +253,9 @@ const ViewCreateModal = ({
name={[name, 'value']}
rules={[{ required: true, message: '请输入内容' }]}
>
{EOperation[filtersValue[name]['operation']] ===
{EOperation[filtersValue?.[name]['operation']] ===
'select' ? (
statusElement(filtersValue[name]['property'])
statusElement(filtersValue?.[name]['property'])
) : (
<Input placeholder="请输入内容" />
)}
+1 -1
View File
@@ -246,7 +246,7 @@ const Dependence = () => {
})
.then(({ code, data }) => {
if (code === 200 && force) {
const i = value.findIndex((x) => x.id === data.data[0].id);
const i = value.findIndex((x) => x.id === data[0].id);
if (i !== -1) {
const result = [...value];
result.splice(i, 1);
+1 -1
View File
@@ -394,7 +394,7 @@ const Env = () => {
if (code === 200) {
const newData = [...value];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
newData.splice(hoverIndex, 0, { ...dragRow, ...data });
setValue([...newData]);
}
});
+4 -2
View File
@@ -13,7 +13,8 @@
.code-box {
position: relative;
display: inline-block;
width: 80%;
width: 80vw;
height: 90vh;
margin: 16px;
background-color: #ffffff;
border: 1px solid rgba(5, 5, 5, 0.06);
@@ -44,9 +45,10 @@
}
.log {
height: calc(100vh - 150px);
height: calc(90vh - 150px);
overflow-y: auto;
padding: 12px;
white-space: pre-line;
}
}
}
+28 -6
View File
@@ -17,9 +17,9 @@ const Error = () => {
needLoading && setLoading(true);
request
.get(`${config.apiPrefix}public/health`)
.then(({ status, error }) => {
if (status === 1) {
return reloadUser();
.then(({ error, data }) => {
if (data?.status === 1) {
return;
}
if (retryTimes.current > 3) {
setData(error?.details);
@@ -27,7 +27,6 @@ const Error = () => {
}
retryTimes.current += 1;
setTimeout(() => {
reloadUser();
getLog(false);
}, 3000);
})
@@ -53,10 +52,33 @@ const Error = () => {
<div className="browser-markup"></div>
<Alert
type="error"
message="服务启动超时,请检查如下日志或者进入容器执行 ql -l check 后刷新再试"
message={
<Typography.Title level={5} type="danger">
</Typography.Title>
}
description={
<Typography.Text type="danger">
<div></div>
<div>
1. 宿 docker run --rm -v
/var/run/docker.sock:/var/run/docker.sock
containrrr/watchtower -cR &lt;&gt;
</div>
<div>2. ql -l checkql -l update</div>
<div>
3. pm2 logs
<Typography.Link href="https://github.com/whyour/qinglong/issues/new?assignees=&labels=&template=bug_report.yml">
issue
</Typography.Link>
</div>
</Typography.Text>
}
banner
/>
<Typography.Paragraph className="log">{data}</Typography.Paragraph>
<Typography.Paragraph code className="log">
{data}
</Typography.Paragraph>
</div>
) : (
<PageLoading tip="启动中,请稍后..." />
+1 -1
View File
@@ -219,7 +219,7 @@ const Initialization = () => {
<Button
type="primary"
onClick={() => {
history.push('/login');
window.location.reload();
}}
>
+4 -2
View File
@@ -1,6 +1,8 @@
const baseUrl = window.__ENV__QlBaseUrl || '/';
export default {
siteName: '青龙',
apiPrefix: '/api/',
apiPrefix: `${baseUrl}api/`,
authKey: 'token',
/* Layout configuration, specify which layout to use for route. */
@@ -91,7 +93,7 @@ export default {
{ value: 'aibotk', label: '智能微秘书' },
{ value: 'iGot', label: 'IGot' },
{ value: 'pushPlus', label: 'PushPlus' },
{ value: 'chat', label: '群chat' },
{ value: 'chat', label: '群chat' },
{ value: 'email', label: '邮箱' },
{ value: 'lark', label: '飞书机器人' },
{ value: 'webhook', label: '自定义通知' },
+10 -3
View File
@@ -4,7 +4,7 @@ import config from './config';
import { history } from '@umijs/max';
message.config({
duration: 1.5,
duration: 2,
});
const time = Date.now();
@@ -23,7 +23,10 @@ const errorHandler = function (error: any) {
history.push('/login');
}
} else {
message.error(msg);
message.error({
content: msg,
style: { maxWidth: 500, margin: '0 auto' },
});
}
} else {
console.log(error.message);
@@ -66,7 +69,11 @@ _request.interceptors.response.use(async (response) => {
const res = await response.clone().json();
if (res.code !== 200) {
const msg = res.message || res.data;
msg && message.error(msg);
msg &&
message.error({
content: msg,
style: { maxWidth: 500, margin: '0 auto' },
});
}
return res;
}
+4
View File
@@ -10,3 +10,7 @@ declare module '*.svg' {
}
declare module 'pstree.remy';
interface Window {
__ENV__QlBaseUrl: string;
}
+5 -10
View File
@@ -1,11 +1,6 @@
version: 2.15.13
changeLogLink: https://t.me/jiao_long/373
version: 2.15.15
changeLogLink: https://t.me/jiao_long/376
changeLog: |
1. 增加运行、停止指定命令接口 system/command-run、system/command-stop
2. 增加容器健康检查
3. 移除执行任务默认超时时间
4. 修改 task 命令生成日志逻辑和关联任务查询
5. 修复更新任务、环境变量、依赖、订阅状态丢失
6. 修改系统通知错误提示
7. 修复系统通知 gotify 配置
8. 其他 bug 修复
1. 支持非跟目录部署,启动时增加参数 -e QlBaseUrl="/test/",参数前后需要斜杠
2. 修复 task 执行可执行文件路径,感谢 https://github.com/jzksnsjswkw
3. 其他 bug 修复