mirror of
https://github.com/whyour/qinglong.git
synced 2025-12-15 08:25:38 +08:00
Merge branch 'develop' into copilot/add-scenario-mode-support
This commit is contained in:
commit
ece9c9186d
|
|
@ -3,7 +3,7 @@ import { Container } from 'typedi';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import SubscriptionService from '../services/subscription';
|
import SubscriptionService from '../services/subscription';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
import cron_parser from 'cron-parser';
|
import { CronExpressionParser } from 'cron-parser';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
|
|
@ -60,7 +60,7 @@ export default (app: Router) => {
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
!req.body.schedule ||
|
!req.body.schedule ||
|
||||||
cron_parser.parseExpression(req.body.schedule).hasNext()
|
CronExpressionParser.parse(req.body.schedule).hasNext()
|
||||||
) {
|
) {
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
const data = await subscriptionService.create(req.body);
|
const data = await subscriptionService.create(req.body);
|
||||||
|
|
@ -193,7 +193,7 @@ export default (app: Router) => {
|
||||||
if (
|
if (
|
||||||
!req.body.schedule ||
|
!req.body.schedule ||
|
||||||
typeof req.body.schedule === 'object' ||
|
typeof req.body.schedule === 'object' ||
|
||||||
cron_parser.parseExpression(req.body.schedule).hasNext()
|
CronExpressionParser.parse(req.body.schedule).hasNext()
|
||||||
) {
|
) {
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
const data = await subscriptionService.update(req.body);
|
const data = await subscriptionService.update(req.body);
|
||||||
|
|
|
||||||
|
|
@ -44,5 +44,5 @@ export const EnvModel = sequelize.define<EnvInstance>('Env', {
|
||||||
position: DataTypes.NUMBER,
|
position: DataTypes.NUMBER,
|
||||||
name: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
name: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
||||||
remarks: DataTypes.STRING,
|
remarks: DataTypes.STRING,
|
||||||
isPinned: { type: DataTypes.NUMBER, field: 'is_pinned' },
|
isPinned: DataTypes.NUMBER,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -64,9 +64,9 @@ export default async () => {
|
||||||
await sequelize.query(
|
await sequelize.query(
|
||||||
'alter table Crontabs add column log_name VARCHAR(255)',
|
'alter table Crontabs add column log_name VARCHAR(255)',
|
||||||
);
|
);
|
||||||
} catch (error) {}
|
} catch (error) { }
|
||||||
try {
|
try {
|
||||||
await sequelize.query('alter table Envs add column is_pinned NUMBER');
|
await sequelize.query('alter table Envs add column isPinned NUMBER');
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
|
|
||||||
Logger.info('✌️ DB loaded');
|
Logger.info('✌️ DB loaded');
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,6 @@ export default async (src: string = 'deps') => {
|
||||||
});
|
});
|
||||||
|
|
||||||
watcher
|
watcher
|
||||||
.on('add', (path) => linkToNodeModule(src))
|
.on('add', () => linkToNodeModule(src))
|
||||||
.on('change', (path) => linkToNodeModule(src));
|
.on('change', () => linkToNodeModule(src));
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import config from '../config';
|
||||||
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||||
import { exec, execSync } from 'child_process';
|
import { exec, execSync } from 'child_process';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import cron_parser from 'cron-parser';
|
import { CronExpressionParser } from 'cron-parser';
|
||||||
import {
|
import {
|
||||||
getFileContentByName,
|
getFileContentByName,
|
||||||
fileExist,
|
fileExist,
|
||||||
|
|
@ -27,7 +27,7 @@ import { ScheduleType } from '../interface/schedule';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CronService {
|
export default class CronService {
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) { }
|
||||||
|
|
||||||
private isNodeCron(cron: Crontab) {
|
private isNodeCron(cron: Crontab) {
|
||||||
const { schedule, extra_schedules } = cron;
|
const { schedule, extra_schedules } = cron;
|
||||||
|
|
@ -49,9 +49,27 @@ export default class CronService {
|
||||||
return this.isOnceSchedule(schedule) || this.isBootSchedule(schedule);
|
return this.isOnceSchedule(schedule) || this.isBootSchedule(schedule);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getLogName(cron: Crontab) {
|
||||||
|
const { log_name, command, id } = cron;
|
||||||
|
if (log_name === '/dev/null') {
|
||||||
|
return log_name;
|
||||||
|
}
|
||||||
|
let uniqPath = await getUniqPath(command, `${id}`);
|
||||||
|
if (log_name) {
|
||||||
|
const normalizedLogName = log_name.startsWith('/') ? log_name : path.join(config.logPath, log_name);
|
||||||
|
if (normalizedLogName.startsWith(config.logPath)) {
|
||||||
|
uniqPath = log_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
|
||||||
|
await fs.mkdir(logDirPath, { recursive: true });
|
||||||
|
return uniqPath;
|
||||||
|
}
|
||||||
|
|
||||||
public async create(payload: Crontab): Promise<Crontab> {
|
public async create(payload: Crontab): Promise<Crontab> {
|
||||||
const tab = new Crontab(payload);
|
const tab = new Crontab(payload);
|
||||||
tab.saved = false;
|
tab.saved = false;
|
||||||
|
tab.log_name = await this.getLogName(tab);
|
||||||
const doc = await this.insert(tab);
|
const doc = await this.insert(tab);
|
||||||
|
|
||||||
if (isDemoEnv()) {
|
if (isDemoEnv()) {
|
||||||
|
|
@ -82,6 +100,7 @@ export default class CronService {
|
||||||
const doc = await this.getDb({ id: payload.id });
|
const doc = await this.getDb({ id: payload.id });
|
||||||
const tab = new Crontab({ ...doc, ...payload });
|
const tab = new Crontab({ ...doc, ...payload });
|
||||||
tab.saved = false;
|
tab.saved = false;
|
||||||
|
tab.log_name = await this.getLogName(tab);
|
||||||
const newDoc = await this.updateDb(tab);
|
const newDoc = await this.updateDb(tab);
|
||||||
|
|
||||||
if (doc.isDisabled === 1 || isDemoEnv()) {
|
if (doc.isDisabled === 1 || isDemoEnv()) {
|
||||||
|
|
@ -142,7 +161,7 @@ export default class CronService {
|
||||||
let cron;
|
let cron;
|
||||||
try {
|
try {
|
||||||
cron = await this.getDb({ id });
|
cron = await this.getDb({ id });
|
||||||
} catch (err) {}
|
} catch (err) { }
|
||||||
if (!cron) {
|
if (!cron) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -476,61 +495,14 @@ export default class CronService {
|
||||||
`[panel][开始执行任务] 参数: ${JSON.stringify(params)}`,
|
`[panel][开始执行任务] 参数: ${JSON.stringify(params)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
let { id, command, log_path, log_name } = cron;
|
let { id, command, log_name } = cron;
|
||||||
|
|
||||||
// Check if log_name is an absolute path
|
const uniqPath = log_name === '/dev/null' ? (await getUniqPath(command, `${id}`)) : log_name;
|
||||||
const isAbsolutePath = log_name && log_name.startsWith('/');
|
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
||||||
|
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
|
||||||
let uniqPath: string;
|
await fs.mkdir(logDirPath, { recursive: true });
|
||||||
let absolutePath: string;
|
const logPath = `${uniqPath}/${logTime}.log`;
|
||||||
let logPath: string;
|
const absolutePath = path.resolve(config.logPath, `${logPath}`);
|
||||||
|
|
||||||
if (isAbsolutePath) {
|
|
||||||
// Special case: /dev/null is allowed as-is to discard logs
|
|
||||||
if (log_name === '/dev/null') {
|
|
||||||
uniqPath = log_name;
|
|
||||||
absolutePath = log_name;
|
|
||||||
logPath = log_name;
|
|
||||||
} else {
|
|
||||||
// For other absolute paths, ensure they are within the safe log directory
|
|
||||||
const normalizedLogName = path.normalize(log_name!);
|
|
||||||
const normalizedLogPath = path.normalize(config.logPath);
|
|
||||||
|
|
||||||
if (!normalizedLogName.startsWith(normalizedLogPath)) {
|
|
||||||
this.logger.error(
|
|
||||||
`[panel][日志路径安全检查失败] 绝对路径必须在日志目录内: ${log_name}`,
|
|
||||||
);
|
|
||||||
// Fallback to auto-generated path for security
|
|
||||||
const fallbackUniqPath = await getUniqPath(command, `${id}`);
|
|
||||||
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
|
||||||
const logDirPath = path.resolve(config.logPath, `${fallbackUniqPath}`);
|
|
||||||
if (log_path?.split('/')?.every((x) => x !== fallbackUniqPath)) {
|
|
||||||
await fs.mkdir(logDirPath, { recursive: true });
|
|
||||||
}
|
|
||||||
logPath = `${fallbackUniqPath}/${logTime}.log`;
|
|
||||||
absolutePath = path.resolve(config.logPath, `${logPath}`);
|
|
||||||
uniqPath = fallbackUniqPath;
|
|
||||||
} else {
|
|
||||||
// Absolute path is safe, use it
|
|
||||||
uniqPath = log_name!;
|
|
||||||
absolutePath = log_name!;
|
|
||||||
logPath = log_name!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Sanitize log_name to prevent path traversal for relative paths
|
|
||||||
const sanitizedLogName = log_name
|
|
||||||
? log_name.replace(/[\/\\\.]/g, '_').replace(/^_+|_+$/g, '')
|
|
||||||
: '';
|
|
||||||
uniqPath = sanitizedLogName || (await getUniqPath(command, `${id}`));
|
|
||||||
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
|
||||||
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
|
|
||||||
if (log_path?.split('/')?.every((x) => x !== uniqPath)) {
|
|
||||||
await fs.mkdir(logDirPath, { recursive: true });
|
|
||||||
}
|
|
||||||
logPath = `${uniqPath}/${logTime}.log`;
|
|
||||||
absolutePath = path.resolve(config.logPath, `${logPath}`);
|
|
||||||
}
|
|
||||||
const cp = spawn(
|
const cp = spawn(
|
||||||
`real_log_path=${logPath} no_delay=true ${this.makeCommand(
|
`real_log_path=${logPath} no_delay=true ${this.makeCommand(
|
||||||
cron,
|
cron,
|
||||||
|
|
@ -610,7 +582,9 @@ export default class CronService {
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
if (doc.log_name === '/dev/null') {
|
||||||
|
return '日志设置为忽略';
|
||||||
|
}
|
||||||
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
|
||||||
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
const logFileExist = doc.log_path && (await fileExist(absolutePath));
|
||||||
if (logFileExist) {
|
if (logFileExist) {
|
||||||
|
|
@ -653,9 +627,7 @@ export default class CronService {
|
||||||
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
|
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
|
||||||
command = `${TASK_PREFIX}${tab.command}`;
|
command = `${TASK_PREFIX}${tab.command}`;
|
||||||
}
|
}
|
||||||
let commandVariable = `real_time=${Boolean(realTime)} no_tee=true ID=${
|
let commandVariable = `real_time=${Boolean(realTime)} log_name=${tab.log_name} no_tee=true ID=${tab.id} `;
|
||||||
tab.id
|
|
||||||
} `;
|
|
||||||
if (tab.task_before) {
|
if (tab.task_before) {
|
||||||
commandVariable += `task_before='${tab.task_before
|
commandVariable += `task_before='${tab.task_before
|
||||||
.replace(/'/g, "'\\''")
|
.replace(/'/g, "'\\''")
|
||||||
|
|
@ -716,7 +688,7 @@ export default class CronService {
|
||||||
if (
|
if (
|
||||||
command &&
|
command &&
|
||||||
schedule &&
|
schedule &&
|
||||||
cron_parser.parseExpression(schedule).hasNext()
|
CronExpressionParser.parse(schedule).hasNext()
|
||||||
) {
|
) {
|
||||||
const name = namePrefix + '_' + index;
|
const name = namePrefix + '_' + index;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Joi } from 'celebrate';
|
import { Joi } from 'celebrate';
|
||||||
import cron_parser from 'cron-parser';
|
import { CronExpressionParser } from 'cron-parser';
|
||||||
import { ScheduleType } from '../interface/schedule';
|
import { ScheduleType } from '../interface/schedule';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
|
@ -13,7 +13,7 @@ const validateSchedule = (value: string, helpers: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (cron_parser.parseExpression(value).hasNext()) {
|
if (CronExpressionParser.parse(value).hasNext()) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -45,27 +45,26 @@ export const commonCronSchema = {
|
||||||
.allow(null)
|
.allow(null)
|
||||||
.custom((value, helpers) => {
|
.custom((value, helpers) => {
|
||||||
if (!value) return value;
|
if (!value) return value;
|
||||||
|
|
||||||
// Check if it's an absolute path
|
// Check if it's an absolute path
|
||||||
if (value.startsWith('/')) {
|
if (value.startsWith('/')) {
|
||||||
// Allow /dev/null as special case
|
// Allow /dev/null as special case
|
||||||
if (value === '/dev/null') {
|
if (value === '/dev/null') {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other absolute paths, ensure they are within the safe log directory
|
// For other absolute paths, ensure they are within the safe log directory
|
||||||
const normalizedValue = path.normalize(value);
|
const normalizedValue = path.normalize(value);
|
||||||
const normalizedLogPath = path.normalize(config.logPath);
|
const normalizedLogPath = path.normalize(config.logPath);
|
||||||
|
|
||||||
if (!normalizedValue.startsWith(normalizedLogPath)) {
|
if (!normalizedValue.startsWith(normalizedLogPath)) {
|
||||||
return helpers.error('string.unsafePath');
|
return helpers.error('string.unsafePath');
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For relative names, enforce strict pattern
|
if (!/^(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?:\/)?(?:[\w.-]+\/)*[\w.-]+\/?$/.test(value)) {
|
||||||
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
|
|
||||||
return helpers.error('string.pattern.base');
|
return helpers.error('string.pattern.base');
|
||||||
}
|
}
|
||||||
if (value.length > 100) {
|
if (value.length > 100) {
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@
|
||||||
"chokidar": "^4.0.1",
|
"chokidar": "^4.0.1",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cron-parser": "^4.9.0",
|
"cron-parser": "^5.4.0",
|
||||||
"cross-spawn": "^7.0.6",
|
"cross-spawn": "^7.0.6",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dotenv": "^16.4.6",
|
"dotenv": "^16.4.6",
|
||||||
|
|
|
||||||
|
|
@ -46,18 +46,22 @@ handle_log_path() {
|
||||||
|
|
||||||
time=$(date "+$mtime_format")
|
time=$(date "+$mtime_format")
|
||||||
log_time=$(format_log_time "$mtime_format" "$time")
|
log_time=$(format_log_time "$mtime_format" "$time")
|
||||||
log_dir_tmp="${file_param##*/}"
|
if [[ -z $log_name ]]; then
|
||||||
if [[ $file_param =~ "/" ]]; then
|
log_dir_tmp="${file_param##*/}"
|
||||||
if [[ $file_param == /* ]]; then
|
if [[ $file_param =~ "/" ]]; then
|
||||||
log_dir_tmp_path="${file_param:1}"
|
if [[ $file_param == /* ]]; then
|
||||||
else
|
log_dir_tmp_path="${file_param:1}"
|
||||||
log_dir_tmp_path="${file_param}"
|
else
|
||||||
|
log_dir_tmp_path="${file_param}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
||||||
|
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||||
|
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||||
|
log_dir="${log_dir_tmp%.*}${suffix}"
|
||||||
|
else
|
||||||
|
log_dir="$log_name"
|
||||||
fi
|
fi
|
||||||
log_dir_tmp_path="${log_dir_tmp_path%/*}"
|
|
||||||
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
|
||||||
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
|
||||||
log_dir="${log_dir_tmp%.*}${suffix}"
|
|
||||||
log_path="$log_dir/$log_time.log"
|
log_path="$log_dir/$log_time.log"
|
||||||
|
|
||||||
if [[ ${real_log_path:=} ]]; then
|
if [[ ${real_log_path:=} ]]; then
|
||||||
|
|
@ -73,6 +77,11 @@ handle_log_path() {
|
||||||
if [[ "${real_time:=}" == "true" ]]; then
|
if [[ "${real_time:=}" == "true" ]]; then
|
||||||
cmd=""
|
cmd=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${log_dir:=}" == "/dev/null" ]]; then
|
||||||
|
cmd=">> /dev/null"
|
||||||
|
log_path="/dev/null"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
format_params() {
|
format_params() {
|
||||||
|
|
|
||||||
17
src/app.ts
17
src/app.ts
|
|
@ -7,11 +7,18 @@ export function rootContainer(container: any) {
|
||||||
'en': require('./locales/en-US.json'),
|
'en': require('./locales/en-US.json'),
|
||||||
'zh': require('./locales/zh-CN.json'),
|
'zh': require('./locales/zh-CN.json'),
|
||||||
};
|
};
|
||||||
let currentLocale = intl.determineLocale({
|
let currentLocale: string;
|
||||||
urlLocaleKey: 'lang',
|
try {
|
||||||
cookieLocaleKey: 'lang',
|
currentLocale = intl.determineLocale({
|
||||||
localStorageLocaleKey: 'lang',
|
urlLocaleKey: 'lang',
|
||||||
}).slice(0, 2);
|
cookieLocaleKey: 'lang',
|
||||||
|
localStorageLocaleKey: 'lang',
|
||||||
|
}).slice(0, 2);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
// Handle decodeURIComponent errors from malformed cookies
|
||||||
|
console.warn('Failed to determine locale from cookies:', e);
|
||||||
|
currentLocale = '';
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentLocale || !Object.keys(locales).includes(currentLocale)) {
|
if (!currentLocale || !Object.keys(locales).includes(currentLocale)) {
|
||||||
currentLocale = 'zh';
|
currentLocale = 'zh';
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ const SHOW_TAB_COUNT = 10;
|
||||||
|
|
||||||
const Crontab = () => {
|
const Crontab = () => {
|
||||||
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
|
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
|
||||||
|
const [allSubscriptions, setAllSubscriptions] = useState<any[]>([]);
|
||||||
const columns: ColumnProps<ICrontab>[] = [
|
const columns: ColumnProps<ICrontab>[] = [
|
||||||
{
|
{
|
||||||
title: intl.get('名称'),
|
title: intl.get('名称'),
|
||||||
|
|
@ -247,8 +248,8 @@ const Crontab = () => {
|
||||||
>
|
>
|
||||||
{record.last_execution_time
|
{record.last_execution_time
|
||||||
? dayjs(record.last_execution_time * 1000).format(
|
? dayjs(record.last_execution_time * 1000).format(
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
)
|
)
|
||||||
: '-'}
|
: '-'}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
@ -272,6 +273,12 @@ const Crontab = () => {
|
||||||
title: intl.get('关联订阅'),
|
title: intl.get('关联订阅'),
|
||||||
width: 185,
|
width: 185,
|
||||||
render: (text, record: any) => record?.subscription?.name || '-',
|
render: (text, record: any) => record?.subscription?.name || '-',
|
||||||
|
key: 'sub_id',
|
||||||
|
dataIndex: 'sub_id',
|
||||||
|
filters: allSubscriptions.map((sub) => ({
|
||||||
|
text: sub.name || sub.alias,
|
||||||
|
value: sub.id,
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: intl.get('操作'),
|
title: intl.get('操作'),
|
||||||
|
|
@ -361,11 +368,10 @@ const Crontab = () => {
|
||||||
const getCrons = () => {
|
const getCrons = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const { page, size, sorter, filters } = pageConf;
|
const { page, size, sorter, filters } = pageConf;
|
||||||
let url = `${
|
let url = `${config.apiPrefix
|
||||||
config.apiPrefix
|
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
|
||||||
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
|
filters,
|
||||||
filters,
|
)}`;
|
||||||
)}`;
|
|
||||||
if (sorter && sorter.column && sorter.order) {
|
if (sorter && sorter.column && sorter.order) {
|
||||||
url += `&sorter=${JSON.stringify({
|
url += `&sorter=${JSON.stringify({
|
||||||
field: sorter.column.key,
|
field: sorter.column.key,
|
||||||
|
|
@ -523,9 +529,8 @@ const Crontab = () => {
|
||||||
|
|
||||||
const enabledOrDisabledCron = (record: any, index: number) => {
|
const enabledOrDisabledCron = (record: any, index: number) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: `确认${
|
title: `确认${record.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')
|
||||||
record.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')
|
}`,
|
||||||
}`,
|
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
{intl.get('确认')}
|
{intl.get('确认')}
|
||||||
|
|
@ -540,8 +545,7 @@ const Crontab = () => {
|
||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.put(
|
.put(
|
||||||
`${config.apiPrefix}crons/${
|
`${config.apiPrefix}crons/${record.isDisabled === 1 ? 'enable' : 'disable'
|
||||||
record.isDisabled === 1 ? 'enable' : 'disable'
|
|
||||||
}`,
|
}`,
|
||||||
[record.id],
|
[record.id],
|
||||||
)
|
)
|
||||||
|
|
@ -565,9 +569,8 @@ const Crontab = () => {
|
||||||
|
|
||||||
const pinOrUnPinCron = (record: any, index: number) => {
|
const pinOrUnPinCron = (record: any, index: number) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: `确认${
|
title: `确认${record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')
|
||||||
record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')
|
}`,
|
||||||
}`,
|
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
{intl.get('确认')}
|
{intl.get('确认')}
|
||||||
|
|
@ -582,8 +585,7 @@ const Crontab = () => {
|
||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.put(
|
.put(
|
||||||
`${config.apiPrefix}crons/${
|
`${config.apiPrefix}crons/${record.isPinned === 1 ? 'unpin' : 'pin'
|
||||||
record.isPinned === 1 ? 'unpin' : 'pin'
|
|
||||||
}`,
|
}`,
|
||||||
[record.id],
|
[record.id],
|
||||||
)
|
)
|
||||||
|
|
@ -799,8 +801,20 @@ const Crontab = () => {
|
||||||
}
|
}
|
||||||
}, [viewConf, enabledCronViews]);
|
}, [viewConf, enabledCronViews]);
|
||||||
|
|
||||||
|
const getAllSubscriptions = () => {
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}subscriptions`)
|
||||||
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
|
setAllSubscriptions(data || []);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getCronViews();
|
getCronViews();
|
||||||
|
getAllSubscriptions();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const viewAction = (key: string) => {
|
const viewAction = (key: string) => {
|
||||||
|
|
@ -1014,6 +1028,7 @@ const Crontab = () => {
|
||||||
)}
|
)}
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
sortDirections={['descend', 'ascend']}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: pageConf.page,
|
current: pageConf.page,
|
||||||
pageSize: pageConf.size,
|
pageSize: pageConf.size,
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ const CronLogModal = ({
|
||||||
const log = data as string;
|
const log = data as string;
|
||||||
setValue(log || intl.get("暂无日志"));
|
setValue(log || intl.get("暂无日志"));
|
||||||
const hasNext = Boolean(
|
const hasNext = Boolean(
|
||||||
log && !logEnded(log) && !log.includes("日志不存在"),
|
log && !logEnded(log) && !log.includes("日志不存在") && !log.includes("日志设置为忽略"),
|
||||||
);
|
);
|
||||||
if (!hasNext && !logEnded(value) && value !== intl.get("启动中...")) {
|
if (!hasNext && !logEnded(value) && value !== intl.get("启动中...")) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
||||||
|
|
@ -190,22 +190,15 @@ const CronModal = ({
|
||||||
{
|
{
|
||||||
validator: (_, value) => {
|
validator: (_, value) => {
|
||||||
if (!value) return Promise.resolve();
|
if (!value) return Promise.resolve();
|
||||||
// Allow /dev/null specifically
|
|
||||||
if (value === '/dev/null') return Promise.resolve();
|
if (value === '/dev/null') return Promise.resolve();
|
||||||
// Warn about other absolute paths (server will validate)
|
if (value.length > 100) {
|
||||||
if (value.startsWith('/')) {
|
return Promise.reject(intl.get('日志名称不能超过100个字符'));
|
||||||
// We can't validate the exact path on frontend, but inform user
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
// For relative names, enforce strict pattern
|
if (!/^(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?:\/)?(?:[\w.-]+\/)*[\w.-]+\/?$/.test(value)) {
|
||||||
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
|
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
intl.get('日志名称只能包含字母、数字、下划线和连字符'),
|
intl.get('日志名称只能包含字母、数字、下划线和连字符'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (value.length > 100) {
|
|
||||||
return Promise.reject(intl.get('日志名称不能超过100个字符'));
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ const Setting = () => {
|
||||||
reloadTheme,
|
reloadTheme,
|
||||||
systemInfo,
|
systemInfo,
|
||||||
} = useOutletContext<SharedContext>();
|
} = useOutletContext<SharedContext>();
|
||||||
|
console.log('user',user)
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: intl.get('名称'),
|
title: intl.get('名称'),
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import cron_parser from 'cron-parser';
|
import { CronExpressionParser } from 'cron-parser';
|
||||||
import isNil from 'lodash/isNil';
|
import isNil from 'lodash/isNil';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
@ -224,8 +224,8 @@ const SubscriptionModal = ({
|
||||||
type === 'raw'
|
type === 'raw'
|
||||||
? 'file'
|
? 'file'
|
||||||
: url.startsWith('http')
|
: url.startsWith('http')
|
||||||
? 'public-repo'
|
? 'public-repo'
|
||||||
: 'private-repo';
|
: 'private-repo';
|
||||||
|
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
type: _type,
|
type: _type,
|
||||||
|
|
@ -381,7 +381,7 @@ const SubscriptionModal = ({
|
||||||
if (
|
if (
|
||||||
scheduleType === 'interval' ||
|
scheduleType === 'interval' ||
|
||||||
!value ||
|
!value ||
|
||||||
cron_parser.parseExpression(value).hasNext()
|
CronExpressionParser.parse(value).hasNext()
|
||||||
) {
|
) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { LANG_MAP, LOG_END_SYMBOL } from './const';
|
import { LANG_MAP, LOG_END_SYMBOL } from './const';
|
||||||
import cron_parser from 'cron-parser';
|
import { CronExpressionParser } from 'cron-parser';
|
||||||
import { ICrontab } from '@/pages/crontab/type';
|
import { ICrontab } from '@/pages/crontab/type';
|
||||||
|
|
||||||
export default function browserType() {
|
export default function browserType() {
|
||||||
|
|
@ -155,9 +155,9 @@ export default function browserType() {
|
||||||
shell === 'none'
|
shell === 'none'
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
shell, // wechat qq uc 360 2345 sougou liebao maxthon
|
shell, // wechat qq uc 360 2345 sougou liebao maxthon
|
||||||
shellVs,
|
shellVs,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
|
|
@ -333,11 +333,11 @@ export function getCommandScript(
|
||||||
|
|
||||||
export function parseCrontab(schedule: string): Date | null {
|
export function parseCrontab(schedule: string): Date | null {
|
||||||
try {
|
try {
|
||||||
const time = cron_parser.parseExpression(schedule);
|
const time = CronExpressionParser.parse(schedule);
|
||||||
if (time) {
|
if (time) {
|
||||||
return time.next().toDate();
|
return time.next().toDate();
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch (error) { }
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user