Compare commits

...
10 Commits
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-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [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> </div>
![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png) ![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
@@ -74,6 +74,14 @@ docker run -dit \
whyour/qinglong:latest 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) ### Docker-compose (Recommended)
```bash ```bash
@@ -105,7 +113,7 @@ podman run -dit \
docker.io/whyour/qinglong:latest 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 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-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [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> </div>
![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png) ![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
@@ -76,6 +76,14 @@ docker run -dit \
whyour/qinglong:latest whyour/qinglong:latest
``` ```
### 宝塔面板一键部署(推荐)
1. 安装宝塔面板,前往 [宝塔面板](https://www.bt.cn/u/EcDAFU) 官网,选择正式版的脚本下载安装
2. 安装后登录宝塔面板,在菜单栏中点击 `Docker`,首次进入会提示安装`Docker`服务,点击立即安装,按提示完成安装
3. 安装完成后在应用商店中找到`青龙面板`,点击安装,配置域名等基本信息即可完成安装
### docker-compose (推荐) ### docker-compose (推荐)
```bash ```bash
@@ -107,7 +115,7 @@ podman run -dit \
docker.io/whyour/qinglong:latest docker.io/whyour/qinglong:latest
``` ```
### npm ### npm (本地)
建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3/pnpm 建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 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 { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
+22 -16
View File
@@ -1,10 +1,9 @@
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config'; import config from '../config';
import { getFileContentByName, readDirs, rmPath } from '../config/util'; import { getFileContentByName, readDirs, rmPath } from '../config/util';
import { join } from 'path'; import { join, resolve } from 'path';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
const blacklist = ['.tmp']; const blacklist = ['.tmp'];
@@ -30,15 +29,19 @@ export default (app: Router) => {
'/detail', '/detail',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { 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: '暂无权限' }); return res.send({ code: 403, message: '暂无权限' });
} }
const filePath = join( const content = await getFileContentByName(finalPath);
config.logPath,
(req.query.path || '') as string,
req.query.file as string,
);
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);
@@ -50,15 +53,18 @@ export default (app: Router) => {
'/:file', '/:file',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { 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: '暂无权限' }); return res.send({ code: 403, message: '暂无权限' });
} }
const filePath = join( const content = await getFileContentByName(finalPath);
config.logPath,
(req.query.path || '') as string,
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);
+1 -8
View File
@@ -1,11 +1,4 @@
import { import { fileExist, readDirs, readDir, rmPath } from '../config/util';
fileExist,
getFileContentByName,
readDirs,
getLastModifyFilePath,
readDir,
rmPath,
} from '../config/util';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
+2
View File
@@ -31,6 +31,7 @@ const tmpPath = path.join(rootPath, '.tmp/');
const samplePath = path.join(rootPath, 'sample/'); const samplePath = path.join(rootPath, 'sample/');
const configPath = path.join(dataPath, 'config/'); const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/'); const scriptPath = path.join(dataPath, 'scripts/');
const repoPath = path.join(dataPath, 'repo/');
const bakPath = path.join(dataPath, 'bak/'); const bakPath = path.join(dataPath, 'bak/');
const logPath = path.join(dataPath, 'log/'); const logPath = path.join(dataPath, 'log/');
const dbPath = path.join(dataPath, 'db/'); const dbPath = path.join(dataPath, 'db/');
@@ -101,6 +102,7 @@ export default {
uploadPath, uploadPath,
configPath, configPath,
scriptPath, scriptPath,
repoPath,
samplePath, samplePath,
blackFileList: [ blackFileList: [
'auth.json', 'auth.json',
+3 -3
View File
@@ -5,13 +5,13 @@ let pickedEnv: Record<string, string>;
function getPickedEnv() { function getPickedEnv() {
if (pickedEnv) return pickedEnv; 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) {
if (!picked.QlBaseUrl.startsWith('/')) { if (!picked.QlBaseUrl.startsWith('/')) {
picked.QlBaseUrl = `/${picked.QlBaseUrl}` picked.QlBaseUrl = `/${picked.QlBaseUrl}`;
} }
if (!picked.QlBaseUrl.endsWith('/')) { if (!picked.QlBaseUrl.endsWith('/')) {
picked.QlBaseUrl = `${picked.QlBaseUrl}/` picked.QlBaseUrl = `${picked.QlBaseUrl}/`;
} }
} }
pickedEnv = picked as Record<string, string>; 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) { public async getFile(filePath: string, res: Response) {
let content = ''; let content = '';
if (config.blackFileList.includes(filePath) || !filePath) { const avaliablePath = [config.rootPath, config.configPath].map((x) =>
res.send({ code: 403, message: '文件无法访问' }); 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/')) { if (filePath.startsWith('sample/')) {
const res = await got.get( const res = await got.get(
`https://gitlab.com/whyour/qinglong/-/raw/master/${filePath}`, `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) { public async getFile(filePath: string, fileName: string) {
let _filePath = join(config.scriptPath, filePath, fileName); const finalPath = path.resolve(config.scriptPath, filePath, fileName);
if (filePath.startsWith(config.dataPath)) {
_filePath = join(filePath, fileName); if (!finalPath.startsWith(config.scriptPath)) {
return '';
} }
const content = await getFileContentByName(_filePath);
const content = await getFileContentByName(finalPath);
return content; return content;
} }
} }
+2
View File
@@ -277,7 +277,9 @@ export default class SubscriptionService {
} }
for (const doc of docs) { for (const doc of docs) {
const filePath = join(config.scriptPath, doc.alias); const filePath = join(config.scriptPath, doc.alias);
const repoPath = join(config.repoPath, doc.alias);
await rmPath(filePath); await rmPath(filePath);
await rmPath(repoPath);
} }
} }
} }
+8 -6
View File
@@ -1143,14 +1143,20 @@ function webhookNotify(text, desp) {
WEBHOOK_CONTENT_TYPE, WEBHOOK_CONTENT_TYPE,
WEBHOOK_METHOD, WEBHOOK_METHOD,
} = push_config; } = 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(); resolve();
return; return;
} }
const headers = parseHeaders(WEBHOOK_HEADERS); const headers = parseHeaders(WEBHOOK_HEADERS);
const body = parseBody(WEBHOOK_BODY, WEBHOOK_CONTENT_TYPE, (v) => 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 bodyParam = formatBodyFun(WEBHOOK_CONTENT_TYPE, body);
const options = { const options = {
@@ -1162,7 +1168,6 @@ function webhookNotify(text, desp) {
retry: 1, retry: 1,
}; };
if (WEBHOOK_METHOD) {
const formatUrl = WEBHOOK_URL.replaceAll( const formatUrl = WEBHOOK_URL.replaceAll(
'$title', '$title',
encodeURIComponent(text), encodeURIComponent(text),
@@ -1180,9 +1185,6 @@ function webhookNotify(text, desp) {
resolve(resp.body); resolve(resp.body);
} }
}); });
} else {
resolve();
}
}); });
} }
+3 -1
View File
@@ -852,7 +852,9 @@ def custom_notify(title: str, content: str) -> None:
body = parse_body( body = parse_body(
WEBHOOK_BODY, WEBHOOK_BODY,
WEBHOOK_CONTENT_TYPE, 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( formatted_url = WEBHOOK_URL.replace(
"$title", urllib.parse.quote_plus(title) "$title", urllib.parse.quote_plus(title)
+16 -8
View File
@@ -216,12 +216,12 @@ check_file() {
if [[ -f $file_env ]]; then if [[ -f $file_env ]]; then
get_env_array get_env_array
if [[ $isJsOrPythonFile == 'true' ]]; then if [[ $isJsOrPythonFile == 'true' ]]; then
PREV_NODE_OPTIONS="${NODE_OPTIONS:=}" export PREV_NODE_OPTIONS="${NODE_OPTIONS:=}"
PREV_PYTHONPATH="${PYTHONPATH:=}" export PREV_PYTHONPATH="${PYTHONPATH:=}"
if [[ $1 == *.js ]] || [[ $1 == *.ts ]] || [[ $1 == *.mjs ]]; then 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 else
export PYTHONPATH="${PYTHONPATH}:${dir_preload}:${dir_config}" export PYTHONPATH="${dir_preload}:${dir_config}:${PYTHONPATH}"
fi fi
else else
. $file_env . $file_env
@@ -229,6 +229,17 @@ check_file() {
fi 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() { main() {
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
if [[ $1 == *.sh ]]; then if [[ $1 == *.sh ]]; then
@@ -270,10 +281,7 @@ if [[ $isJsOrPythonFile == 'false' ]]; then
run_task_before "${task_shell_params[@]}" run_task_before "${task_shell_params[@]}"
fi fi
set_u_on="false" set_u_on="false"
if set -o | grep -q 'nounset.*on'; then check_nounset
set_u_on="true"
set +u
fi
main "${task_shell_params[@]}" main "${task_shell_params[@]}"
if [[ "$set_u_on" == 'true' ]]; then if [[ "$set_u_on" == 'true' ]]; then
set -u set -u
+15 -4
View File
@@ -1,5 +1,6 @@
const { execSync } = require('child_process'); const { execSync } = require('child_process');
require(`./env.js`); require(`./env.js`);
function expandRange(rangeStr, max) { function expandRange(rangeStr, max) {
const tempRangeStr = rangeStr const tempRangeStr = rangeStr
.trim() .trim()
@@ -28,11 +29,12 @@ function run() {
file_task_before_js, file_task_before_js,
dir_scripts, dir_scripts,
task_before, task_before,
PREV_NODE_OPTIONS
} = process.env; } = process.env;
require(file_task_before_js);
try { try {
process.env.NODE_OPTIONS = PREV_NODE_OPTIONS;
const splitStr = '__sitecustomize__'; const splitStr = '__sitecustomize__';
const fileName = process.argv[1].replace(`${dir_scripts}/`, ''); const fileName = process.argv[1].replace(`${dir_scripts}/`, '');
let command = `bash -c "source ${file_task_before} ${fileName}`; let command = `bash -c "source ${file_task_before} ${fileName}`;
@@ -44,7 +46,7 @@ function run() {
console.log('执行前置命令\n'); console.log('执行前置命令\n');
} }
const res = execSync( 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', encoding: 'utf-8',
}, },
@@ -60,10 +62,19 @@ function run() {
} }
} catch (error) { } catch (error) {
if (!error.message.includes('spawnSync /bin/sh E2BIG')) { 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) { if (envParam && numParam) {
const array = (process.env[envParam] || '').split('&'); const array = (process.env[envParam] || '').split('&');
const runArr = expandRange(numParam, array.length); const runArr = expandRange(numParam, array.length);
+16 -4
View File
@@ -35,9 +35,10 @@ def expand_range(range_str, max_value):
def run(): def run():
import task_before
try: try:
prev_pythonpath = os.getenv("PREV_PYTHONPATH", "")
os.environ["PYTHONPATH"] = prev_pythonpath
split_str = "__sitecustomize__" split_str = "__sitecustomize__"
file_name = sys.argv[0].replace(f"{os.getenv('dir_scripts')}/", "") file_name = sys.argv[0].replace(f"{os.getenv('dir_scripts')}/", "")
command = f'bash -c "source {os.getenv("file_task_before")} {file_name}' command = f'bash -c "source {os.getenv("file_task_before")} {file_name}'
@@ -48,7 +49,10 @@ def run():
command += f" && eval '{escape_task_before}'" command += f" && eval '{escape_task_before}'"
print("执行前置命令\n") 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}\"" command += f" && echo -e '{split_str}' && {python_command}\""
res = subprocess.check_output(command, shell=True, encoding="utf-8") res = subprocess.check_output(command, shell=True, encoding="utf-8")
@@ -68,10 +72,18 @@ def run():
except OSError as error: except OSError as error:
error_message = str(error) error_message = str(error)
if "Argument list too long" not in error_message: 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: except Exception as error:
print(f"run task before error: {error}") print(f"run task before error: {error}")
import task_before
env_param = os.getenv("envParam") env_param = os.getenv("envParam")
num_param = os.getenv("numParam") num_param = os.getenv("numParam")
+4
View File
@@ -856,6 +856,10 @@ const Crontab = () => {
icon: <SettingOutlined />, icon: <SettingOutlined />,
}, },
], ],
style: {
maxHeight: 350,
overflowY: 'auto',
},
}; };
const getCronViews = () => { const getCronViews = () => {
+2 -1
View File
@@ -108,6 +108,7 @@ const Script = () => {
}; };
const item = findNode(_data, (c) => c.key === obj.node.key); const item = findNode(_data, (c) => c.key === obj.node.key);
if (item) { if (item) {
obj.node = item;
setExpandedKeys([p as string]); setExpandedKeys([p as string]);
onTreeSelect([vkey], obj); onTreeSelect([vkey], obj);
} }
@@ -643,7 +644,7 @@ const Script = () => {
fontSize: 12, fontSize: 12,
lineNumbersMinChars: 3, lineNumbersMinChars: 3,
glyphMargin: false, glyphMargin: false,
accessibilitySupport: 'off' accessibilitySupport: 'off',
}} }}
onMount={(editor) => { onMount={(editor) => {
editorRef.current = editor; editorRef.current = editor;
+3 -2
View File
@@ -313,8 +313,9 @@ export function getCommandScript(
['.js', '.ts', '.sh', '.py'].some((y) => x.endsWith(y)), ['.js', '.ts', '.sh', '.py'].some((y) => x.endsWith(y)),
); );
if (!scriptsPart) return; if (!scriptsPart) return;
if (scriptsPart.startsWith('/ql/data/scripts')) { const scriptDir = `${window.__ENV__QL_DIR}/data/scripts`;
scriptsPart = scriptsPart.replace('/ql/data/scripts/', ''); if (scriptsPart.startsWith(scriptDir)) {
scriptsPart = scriptsPart.replace(scriptDir, '');
} }
let p: string, s: string; let p: string, s: string;
+1
View File
@@ -12,4 +12,5 @@ declare module '*.svg' {
interface Window { interface Window {
__ENV__QlBaseUrl: string; __ENV__QlBaseUrl: string;
__ENV__DeployEnv: string; __ENV__DeployEnv: string;
__ENV__QL_DIR: string;
} }
+7 -11
View File
@@ -1,12 +1,8 @@
version: 2.17.10 version: 2.17.11
changeLogLink: https://t.me/jiao_long/421 changeLogLink: https://t.me/jiao_long/422
publishTime: 2024-08-30 23:00 publishTime: 2024-09-13 23:00
changeLog: | changeLog: |
1. 系统日志增加时间筛选和清空操作 1. 修复无法获取设置的环境变量 PYTHONPATH 和 NODE_OPTIONS
2. 单个任务运行中实例限制最多 5 个,防止任务堆积过多,运行实例超过 5 个时,会发频繁通知 2. 修复定时任务视图过多时,无法看到视图管理
3. 修复 shell 任务无法正常结束 3. 修复自定义通知 json 解析
4. 修改 python 任务 smtp 参数说明,详见 config.sh 4. 修复可能产生目录遍历攻击 API
5. 修复任务执行前命令 (task_before) 执行报错
6. 修复删除日志命令可能失败
7. 其他样式修复