Compare commits

..

10 Commits

Author SHA1 Message Date
whyour 4c5223ba11 更新版本 v2.17.11 2024-09-16 21:35:38 +08:00
whyour 2f70e10be7 修复 shell 检查 nounset 变量 2024-09-16 21:35:36 +08:00
whyour 66706ce150 修复任务视图过多无法选到视图管理 2024-09-10 22:50:33 +08:00
whyour 3d719baa54 修复任务执行初始化环境变量 PYTHONPATH 和 NODE_OPTIONS 被清空 2024-09-10 22:41:39 +08:00
whyour 926ad067c1 修复通知文件换行符解析错误 2024-09-09 23:19:58 +08:00
whyour ff98c3a499 修复任务跳转脚本未显示文件大小,订阅删除未删除 repo 目录 2024-09-08 21:08:15 +08:00
whyour 69fcf45231 修复任务执行前命令错误提示 2024-09-08 17:58:37 +08:00
whyour c3704502bf 更新 readme 2024-09-05 22:35:14 +08:00
whyour a0613d0f39 修复文件越权访问 2024-09-04 23:25:48 +08:00
whyour 8c0f46420e 更新 readme 2024-09-04 22:27:03 +08:00
20 changed files with 163 additions and 93 deletions
+11 -3
View File
@@ -18,9 +18,9 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
[Demo](http://demo.dlww.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
[Demo](http://demo.ninesix.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
[演示](http://demo.dlww.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
[演示](http://demo.ninesix.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
</div>
![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
@@ -74,6 +74,14 @@ docker run -dit \
whyour/qinglong:latest
```
### BaoTa Panel one-click deployment (Recommended)
1. To install Pagoda Panel, go to the official website of [BaoTa Panel](https://www.bt.cn/u/EcDAFU), select the official version of the script to download and install.
2. After installation, login to Pagoda Panel, click `Docker` in the menu bar, the first time you enter, you will be prompted to install `Docker` service, click Install Now, follow the prompts to complete the installation.
3. After the installation is complete, find `Qinglong Panel` in the app shop, click Install, configure the domain name and other basic information to complete the installation.
### Docker-compose (Recommended)
```bash
@@ -105,7 +113,7 @@ podman run -dit \
docker.io/whyour/qinglong:latest
```
### Npm
### Npm (Local)
It is recommended to use a pure system installation to avoid losing the original system data, you need to install node/npm/python3/pip3/pnpm yourself
+11 -3
View File
@@ -20,9 +20,9 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
[Demo](http://demo.dlww.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
[Demo](http://demo.ninesix.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
[演示](http://demo.dlww.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
[演示](http://demo.ninesix.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
</div>
![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
@@ -76,6 +76,14 @@ docker run -dit \
whyour/qinglong:latest
```
### 宝塔面板一键部署(推荐)
1. 安装宝塔面板,前往 [宝塔面板](https://www.bt.cn/u/EcDAFU) 官网,选择正式版的脚本下载安装
2. 安装后登录宝塔面板,在菜单栏中点击 `Docker`,首次进入会提示安装`Docker`服务,点击立即安装,按提示完成安装
3. 安装完成后在应用商店中找到`青龙面板`,点击安装,配置域名等基本信息即可完成安装
### docker-compose (推荐)
```bash
@@ -107,7 +115,7 @@ podman run -dit \
docker.io/whyour/qinglong:latest
```
### npm
### npm (本地)
建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3/pnpm
-1
View File
@@ -1,4 +1,3 @@
import { getFileContentByName, getLastModifyFilePath } from '../config/util';
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
+22 -16
View File
@@ -1,10 +1,9 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import { getFileContentByName, readDirs, rmPath } from '../config/util';
import { join } from 'path';
import { join, resolve } from 'path';
import { celebrate, Joi } from 'celebrate';
const route = Router();
const blacklist = ['.tmp'];
@@ -30,15 +29,19 @@ export default (app: Router) => {
'/detail',
async (req: Request, res: Response, next: NextFunction) => {
try {
if (blacklist.includes(req.path)) {
const finalPath = resolve(
config.logPath,
(req.query.path as string) || '',
(req.query.file as string) || '',
);
if (
blacklist.includes(req.query.path as string) ||
!finalPath.startsWith(config.logPath)
) {
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);
const content = await getFileContentByName(finalPath);
res.send({ code: 200, data: content });
} catch (e) {
return next(e);
@@ -50,15 +53,18 @@ export default (app: Router) => {
'/:file',
async (req: Request, res: Response, next: NextFunction) => {
try {
if (blacklist.includes(req.path)) {
const finalPath = resolve(
config.logPath,
(req.query.path as string) || '',
(req.params.file as string) || '',
);
if (
blacklist.includes(req.path) ||
!finalPath.startsWith(config.logPath)
) {
return res.send({ code: 403, message: '暂无权限' });
}
const filePath = join(
config.logPath,
(req.query.path || '') as string,
req.params.file,
);
const content = await getFileContentByName(filePath);
const content = await getFileContentByName(finalPath);
res.send({ code: 200, data: content });
} catch (e) {
return next(e);
+1 -8
View File
@@ -1,11 +1,4 @@
import {
fileExist,
getFileContentByName,
readDirs,
getLastModifyFilePath,
readDir,
rmPath,
} from '../config/util';
import { fileExist, readDirs, readDir, rmPath } from '../config/util';
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
+2
View File
@@ -31,6 +31,7 @@ const tmpPath = path.join(rootPath, '.tmp/');
const samplePath = path.join(rootPath, 'sample/');
const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/');
const repoPath = path.join(dataPath, 'repo/');
const bakPath = path.join(dataPath, 'bak/');
const logPath = path.join(dataPath, 'log/');
const dbPath = path.join(dataPath, 'db/');
@@ -101,6 +102,7 @@ export default {
uploadPath,
configPath,
scriptPath,
repoPath,
samplePath,
blackFileList: [
'auth.json',
+3 -3
View File
@@ -5,13 +5,13 @@ let pickedEnv: Record<string, string>;
function getPickedEnv() {
if (pickedEnv) return pickedEnv;
const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv']);
const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv', 'QL_DIR']);
if (picked.QlBaseUrl) {
if (!picked.QlBaseUrl.startsWith('/')) {
picked.QlBaseUrl = `/${picked.QlBaseUrl}`
picked.QlBaseUrl = `/${picked.QlBaseUrl}`;
}
if (!picked.QlBaseUrl.endsWith('/')) {
picked.QlBaseUrl = `${picked.QlBaseUrl}/`
picked.QlBaseUrl = `${picked.QlBaseUrl}/`;
}
}
pickedEnv = picked as Record<string, string>;
+14 -2
View File
@@ -11,9 +11,21 @@ export default class ConfigService {
public async getFile(filePath: string, res: Response) {
let content = '';
if (config.blackFileList.includes(filePath) || !filePath) {
res.send({ code: 403, message: '文件无法访问' });
const avaliablePath = [config.rootPath, config.configPath].map((x) =>
path.resolve(x, filePath),
);
if (
config.blackFileList.includes(filePath) ||
avaliablePath.every(
(x) =>
!x.startsWith(config.scriptPath) && !x.startsWith(config.configPath),
) ||
!filePath
) {
return res.send({ code: 403, message: '文件无法访问' });
}
if (filePath.startsWith('sample/')) {
const res = await got.get(
`https://gitlab.com/whyour/qinglong/-/raw/master/${filePath}`,
+6 -4
View File
@@ -65,11 +65,13 @@ export default class ScriptService {
}
public async getFile(filePath: string, fileName: string) {
let _filePath = join(config.scriptPath, filePath, fileName);
if (filePath.startsWith(config.dataPath)) {
_filePath = join(filePath, fileName);
const finalPath = path.resolve(config.scriptPath, filePath, fileName);
if (!finalPath.startsWith(config.scriptPath)) {
return '';
}
const content = await getFileContentByName(_filePath);
const content = await getFileContentByName(finalPath);
return content;
}
}
+2
View File
@@ -277,7 +277,9 @@ export default class SubscriptionService {
}
for (const doc of docs) {
const filePath = join(config.scriptPath, doc.alias);
const repoPath = join(config.repoPath, doc.alias);
await rmPath(filePath);
await rmPath(repoPath);
}
}
}
+24 -22
View File
@@ -1143,14 +1143,20 @@ function webhookNotify(text, desp) {
WEBHOOK_CONTENT_TYPE,
WEBHOOK_METHOD,
} = push_config;
if (!WEBHOOK_URL.includes('$title') && !WEBHOOK_BODY.includes('$title')) {
if (
!WEBHOOK_METHOD ||
!WEBHOOK_URL ||
(!WEBHOOK_URL.includes('$title') && !WEBHOOK_BODY.includes('$title'))
) {
resolve();
return;
}
const headers = parseHeaders(WEBHOOK_HEADERS);
const body = parseBody(WEBHOOK_BODY, WEBHOOK_CONTENT_TYPE, (v) =>
v?.replaceAll('$title', text)?.replaceAll('$content', desp),
v
?.replaceAll('$title', text?.replaceAll('\n', '\\n'))
?.replaceAll('$content', desp?.replaceAll('\n', '\\n')),
);
const bodyParam = formatBodyFun(WEBHOOK_CONTENT_TYPE, body);
const options = {
@@ -1162,27 +1168,23 @@ function webhookNotify(text, desp) {
retry: 1,
};
if (WEBHOOK_METHOD) {
const formatUrl = WEBHOOK_URL.replaceAll(
'$title',
encodeURIComponent(text),
).replaceAll('$content', encodeURIComponent(desp));
got(formatUrl, options).then((resp) => {
try {
if (resp.statusCode !== 200) {
console.log(`自定义发送通知消息失败😞 ${resp.body}\n`);
} else {
console.log(`自定义发送通知消息成功🎉 ${resp.body}\n`);
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(resp.body);
const formatUrl = WEBHOOK_URL.replaceAll(
'$title',
encodeURIComponent(text),
).replaceAll('$content', encodeURIComponent(desp));
got(formatUrl, options).then((resp) => {
try {
if (resp.statusCode !== 200) {
console.log(`自定义发送通知消息失败😞 ${resp.body}\n`);
} else {
console.log(`自定义发送通知消息成功🎉 ${resp.body}\n`);
}
});
} else {
resolve();
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(resp.body);
}
});
});
}
+3 -1
View File
@@ -852,7 +852,9 @@ def custom_notify(title: str, content: str) -> None:
body = parse_body(
WEBHOOK_BODY,
WEBHOOK_CONTENT_TYPE,
lambda v: v.replace("$title", title).replace("$content", content),
lambda v: v.replace("$title", title.replace("\n", "\\n")).replace(
"$content", content.replace("\n", "\\n")
),
)
formatted_url = WEBHOOK_URL.replace(
"$title", urllib.parse.quote_plus(title)
+16 -8
View File
@@ -216,12 +216,12 @@ check_file() {
if [[ -f $file_env ]]; then
get_env_array
if [[ $isJsOrPythonFile == 'true' ]]; then
PREV_NODE_OPTIONS="${NODE_OPTIONS:=}"
PREV_PYTHONPATH="${PYTHONPATH:=}"
export PREV_NODE_OPTIONS="${NODE_OPTIONS:=}"
export PREV_PYTHONPATH="${PYTHONPATH:=}"
if [[ $1 == *.js ]] || [[ $1 == *.ts ]] || [[ $1 == *.mjs ]]; then
export NODE_OPTIONS="${NODE_OPTIONS} -r ${file_preload_js}"
export NODE_OPTIONS="-r ${file_preload_js} ${NODE_OPTIONS}"
else
export PYTHONPATH="${PYTHONPATH}:${dir_preload}:${dir_config}"
export PYTHONPATH="${dir_preload}:${dir_config}:${PYTHONPATH}"
fi
else
. $file_env
@@ -229,6 +229,17 @@ check_file() {
fi
}
check_nounset() {
local output=$(set -o)
while read -r line; do
if [[ "$line" =~ "nounset" ]] && [[ "$line" =~ "on" ]]; then
set_u_on="true"
set +u
break
fi
done <<<"$output"
}
main() {
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
if [[ $1 == *.sh ]]; then
@@ -270,10 +281,7 @@ if [[ $isJsOrPythonFile == 'false' ]]; then
run_task_before "${task_shell_params[@]}"
fi
set_u_on="false"
if set -o | grep -q 'nounset.*on'; then
set_u_on="true"
set +u
fi
check_nounset
main "${task_shell_params[@]}"
if [[ "$set_u_on" == 'true' ]]; then
set -u
+15 -4
View File
@@ -1,5 +1,6 @@
const { execSync } = require('child_process');
require(`./env.js`);
function expandRange(rangeStr, max) {
const tempRangeStr = rangeStr
.trim()
@@ -28,11 +29,12 @@ function run() {
file_task_before_js,
dir_scripts,
task_before,
PREV_NODE_OPTIONS
} = process.env;
require(file_task_before_js);
try {
process.env.NODE_OPTIONS = PREV_NODE_OPTIONS;
const splitStr = '__sitecustomize__';
const fileName = process.argv[1].replace(`${dir_scripts}/`, '');
let command = `bash -c "source ${file_task_before} ${fileName}`;
@@ -44,7 +46,7 @@ function run() {
console.log('执行前置命令\n');
}
const res = execSync(
`${command} && echo -e '${splitStr}' && NODE_OPTIONS= node -p 'JSON.stringify(process.env)'"`,
`${command} && echo -e '${splitStr}' && node -p 'JSON.stringify(process.env)'"`,
{
encoding: 'utf-8',
},
@@ -60,10 +62,19 @@ function run() {
}
} catch (error) {
if (!error.message.includes('spawnSync /bin/sh E2BIG')) {
console.log(`run task before error: `, error);
console.log(`run task before error: `, error);
} else {
console.log(
`❌ The environment variable is too large. It is recommended to use task_before.js instead of task_before.sh\n`,
);
}
if (task_before) {
console.log('执行前置命令结束\n');
}
}
require(file_task_before_js);
if (envParam && numParam) {
const array = (process.env[envParam] || '').split('&');
const runArr = expandRange(numParam, array.length);
+16 -4
View File
@@ -35,9 +35,10 @@ def expand_range(range_str, max_value):
def run():
import task_before
try:
prev_pythonpath = os.getenv("PREV_PYTHONPATH", "")
os.environ["PYTHONPATH"] = prev_pythonpath
split_str = "__sitecustomize__"
file_name = sys.argv[0].replace(f"{os.getenv('dir_scripts')}/", "")
command = f'bash -c "source {os.getenv("file_task_before")} {file_name}'
@@ -48,7 +49,10 @@ def run():
command += f" && eval '{escape_task_before}'"
print("执行前置命令\n")
python_command = "PYTHONPATH= python3 -c 'import os, json; print(json.dumps(dict(os.environ)))'"
prev_pythonpath = os.getenv("PREV_PYTHONPATH", "")
python_command = (
"python3 -c 'import os, json; print(json.dumps(dict(os.environ)))'"
)
command += f" && echo -e '{split_str}' && {python_command}\""
res = subprocess.check_output(command, shell=True, encoding="utf-8")
@@ -68,10 +72,18 @@ def run():
except OSError as error:
error_message = str(error)
if "Argument list too long" not in error_message:
print(f"run task before error: {error}")
print(f"run task before error: {error}")
else:
print(
"❌ The environment variable is too large. It is recommended to use task_before.py instead of task_before.sh\n"
)
if task_before:
print("执行前置命令结束")
except Exception as error:
print(f"run task before error: {error}")
import task_before
env_param = os.getenv("envParam")
num_param = os.getenv("numParam")
+4
View File
@@ -856,6 +856,10 @@ const Crontab = () => {
icon: <SettingOutlined />,
},
],
style: {
maxHeight: 350,
overflowY: 'auto',
},
};
const getCronViews = () => {
+2 -1
View File
@@ -108,6 +108,7 @@ const Script = () => {
};
const item = findNode(_data, (c) => c.key === obj.node.key);
if (item) {
obj.node = item;
setExpandedKeys([p as string]);
onTreeSelect([vkey], obj);
}
@@ -643,7 +644,7 @@ const Script = () => {
fontSize: 12,
lineNumbersMinChars: 3,
glyphMargin: false,
accessibilitySupport: 'off'
accessibilitySupport: 'off',
}}
onMount={(editor) => {
editorRef.current = editor;
+3 -2
View File
@@ -313,8 +313,9 @@ export function getCommandScript(
['.js', '.ts', '.sh', '.py'].some((y) => x.endsWith(y)),
);
if (!scriptsPart) return;
if (scriptsPart.startsWith('/ql/data/scripts')) {
scriptsPart = scriptsPart.replace('/ql/data/scripts/', '');
const scriptDir = `${window.__ENV__QL_DIR}/data/scripts`;
if (scriptsPart.startsWith(scriptDir)) {
scriptsPart = scriptsPart.replace(scriptDir, '');
}
let p: string, s: string;
+1
View File
@@ -12,4 +12,5 @@ declare module '*.svg' {
interface Window {
__ENV__QlBaseUrl: string;
__ENV__DeployEnv: string;
__ENV__QL_DIR: string;
}
+7 -11
View File
@@ -1,12 +1,8 @@
version: 2.17.10
changeLogLink: https://t.me/jiao_long/421
publishTime: 2024-08-30 23:00
version: 2.17.11
changeLogLink: https://t.me/jiao_long/422
publishTime: 2024-09-13 23:00
changeLog: |
1. 系统日志增加时间筛选和清空操作
2. 单个任务运行中实例限制最多 5 个,防止任务堆积过多,运行实例超过 5 个时,会发频繁通知
3. 修复 shell 任务无法正常结束
4. 修改 python 任务 smtp 参数说明,详见 config.sh
5. 修复任务执行前命令 (task_before) 执行报错
6. 修复删除日志命令可能失败
7. 其他样式修复
1. 修复无法获取设置的环境变量 PYTHONPATH 和 NODE_OPTIONS
2. 修复定时任务视图过多时,无法看到视图管理
3. 修复自定义通知 json 解析
4. 修复可能产生目录遍历攻击 API