mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-09 02:14:33 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c5223ba11 | |||
| 2f70e10be7 | |||
| 66706ce150 | |||
| 3d719baa54 | |||
| 926ad067c1 | |||
| ff98c3a499 | |||
| 69fcf45231 | |||
| c3704502bf | |||
| a0613d0f39 | |||
| 8c0f46420e |
+11
-3
@@ -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>
|
||||
|
||||

|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||

|
||||
@@ -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,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
@@ -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
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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}`,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -856,6 +856,10 @@ const Crontab = () => {
|
||||
icon: <SettingOutlined />,
|
||||
},
|
||||
],
|
||||
style: {
|
||||
maxHeight: 350,
|
||||
overflowY: 'auto',
|
||||
},
|
||||
};
|
||||
|
||||
const getCronViews = () => {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Vendored
+1
@@ -12,4 +12,5 @@ declare module '*.svg' {
|
||||
interface Window {
|
||||
__ENV__QlBaseUrl: string;
|
||||
__ENV__DeployEnv: string;
|
||||
__ENV__QL_DIR: string;
|
||||
}
|
||||
|
||||
+7
-11
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user