feat: implement cron-based automation task scheduling and update task configuration

This commit is contained in:
shiyu
2026-01-15 15:04:10 +08:00
parent 3e1b75d81a
commit ab89451b2d
14 changed files with 232 additions and 60 deletions

View File

@@ -5,8 +5,7 @@ export interface AutomationTask {
id: number;
name: string;
event: string;
path_pattern?: string;
filename_regex?: string;
trigger_config?: Record<string, any>;
processor_type: string;
processor_config: Record<string, any>;
enabled: boolean;

View File

@@ -521,9 +521,12 @@
"Trigger Event": "Trigger Event",
"File Written": "File Written",
"File Deleted": "File Deleted",
"Scheduled": "Scheduled",
"Matching Rules": "Matching Rules",
"Path Prefix (optional)": "Path Prefix (optional)",
"Filename Regex (optional)": "Filename Regex (optional)",
"Schedule": "Schedule",
"Cron Expression": "Cron Expression",
"Action": "Action",
"Current Task Queue": "Current Task Queue",
"Params": "Params",

View File

@@ -512,9 +512,12 @@
"Trigger Event": "触发事件",
"File Written": "文件写入",
"File Deleted": "文件删除",
"Scheduled": "定时任务",
"Matching Rules": "匹配规则",
"Path Prefix (optional)": "路径前缀 (可选)",
"Filename Regex (optional)": "文件名正则 (可选)",
"Schedule": "定时设置",
"Cron Expression": "Cron 表达式",
"Action": "执行动作",
"Current Task Queue": "当前任务队列",
"Params": "参数",
@@ -646,7 +649,6 @@
"Created (newest)": "创建时间(最新)",
"Installed already": "已安装",
"No results": "暂无结果",
"Downloading": "下载中",
"Download and Install": "下载并安装",
"Loading apps": "加载应用中",
"Failed to load apps": "加载应用失败",

View File

@@ -15,7 +15,7 @@ const TasksPage = memo(function TasksPage() {
const [form] = Form.useForm();
const [availableProcessors, setAvailableProcessors] = useState<ProcessorTypeMeta[]>([]);
const { t } = useI18n();
const [pathPickerOpen, setPathPickerOpen] = useState(false);
const [pathPickerField, setPathPickerField] = useState<'path_prefix' | 'cron_path' | null>(null);
const fetchList = useCallback(async () => {
setLoading(true);
@@ -42,7 +42,8 @@ const TasksPage = memo(function TasksPage() {
name: '',
event: 'file_written',
enabled: true,
processor_config: {}
processor_config: {},
trigger_config: {}
});
setOpen(true);
};
@@ -52,7 +53,8 @@ const TasksPage = memo(function TasksPage() {
form.resetFields();
form.setFieldsValue({
...rec,
processor_config: rec.processor_config || {}
processor_config: rec.processor_config || {},
trigger_config: rec.trigger_config || {}
});
setOpen(true);
};
@@ -60,7 +62,15 @@ const TasksPage = memo(function TasksPage() {
const submit = async () => {
try {
const values = await form.validateFields();
const body = { ...values };
const triggerConfig = { ...(values.trigger_config || {}) };
if (values.event === 'cron') {
delete triggerConfig.path_prefix;
delete triggerConfig.filename_regex;
} else {
delete triggerConfig.cron_expr;
delete triggerConfig.path;
}
const body = { ...values, trigger_config: triggerConfig };
setLoading(true);
if (editing) {
await tasksApi.update(editing.id, body);
@@ -133,7 +143,10 @@ const TasksPage = memo(function TasksPage() {
const selectedProcessor = Form.useWatch('processor_type', form);
const currentProcessorMeta = availableProcessors.find(p => p.type === selectedProcessor);
const watchedPathPattern = Form.useWatch('path_pattern', form);
const selectedEvent = Form.useWatch('event', form);
const watchedPathPrefix = Form.useWatch(['trigger_config', 'path_prefix'], form);
const watchedCronPath = Form.useWatch(['trigger_config', 'path'], form);
const isCron = selectedEvent === 'cron';
return (
@@ -158,11 +171,11 @@ const TasksPage = memo(function TasksPage() {
title={editing ? `${t('Edit Task')}: ${editing.name}` : t('Create Automation Task')}
width={480}
open={open}
onClose={() => { setOpen(false); setEditing(null); }}
onClose={() => { setOpen(false); setEditing(null); setPathPickerField(null); }}
destroyOnHidden
extra={
<Space>
<Button onClick={() => { setOpen(false); setEditing(null); }}>{t('Cancel')}</Button>
<Button onClick={() => { setOpen(false); setEditing(null); setPathPickerField(null); }}>{t('Cancel')}</Button>
<Button type="primary" onClick={submit} loading={loading}>{t('Submit')}</Button>
</Space>
}
@@ -174,19 +187,45 @@ const TasksPage = memo(function TasksPage() {
<Form.Item name="event" label={t('Trigger Event')} rules={[{ required: true }]}>
<Select options={[
{ value: 'file_written', label: t('File Written') },
{ value: 'file_deleted', label: t('File Deleted') },
{ value: 'file_deleted', label: t('File Deleted') },
{ value: 'cron', label: t('Scheduled') },
]} />
</Form.Item>
<Typography.Title level={5} style={{ marginTop: 8, fontSize: 14 }}>{t('Matching Rules')}</Typography.Title>
<Form.Item name="path_pattern" label={t('Path Prefix (optional)')}>
<Input
placeholder="/images/screenshots"
addonAfter={<Button size="small" onClick={() => setPathPickerOpen(true)}>{t('Select')}</Button>}
/>
</Form.Item>
<Form.Item name="filename_regex" label={t('Filename Regex (optional)')}>
<Input placeholder=".*\.png$" />
</Form.Item>
{isCron ? (
<>
<Typography.Title level={5} style={{ marginTop: 8, fontSize: 14 }}>{t('Schedule')}</Typography.Title>
<Form.Item
name={['trigger_config', 'cron_expr']}
label={t('Cron Expression')}
rules={[{ required: true }]}
>
<Input placeholder="*/5 * * * * *" />
</Form.Item>
<Form.Item
name={['trigger_config', 'path']}
label={t('Target Path')}
rules={[{ required: true }]}
>
<Input
placeholder="/images"
addonAfter={<Button size="small" onClick={() => setPathPickerField('cron_path')}>{t('Select')}</Button>}
/>
</Form.Item>
</>
) : (
<>
<Typography.Title level={5} style={{ marginTop: 8, fontSize: 14 }}>{t('Matching Rules')}</Typography.Title>
<Form.Item name={['trigger_config', 'path_prefix']} label={t('Path Prefix (optional)')}>
<Input
placeholder="/images/screenshots"
addonAfter={<Button size="small" onClick={() => setPathPickerField('path_prefix')}>{t('Select')}</Button>}
/>
</Form.Item>
<Form.Item name={['trigger_config', 'filename_regex']} label={t('Filename Regex (optional)')}>
<Input placeholder=".*\\.png$" />
</Form.Item>
</>
)}
<Form.Item name="enabled" label={t('Enabled')} valuePropName="checked">
<Switch />
</Form.Item>
@@ -205,11 +244,18 @@ const TasksPage = memo(function TasksPage() {
</Form>
</Drawer>
<PathSelectorModal
open={pathPickerOpen}
mode="directory"
initialPath={watchedPathPattern || '/'}
onCancel={() => setPathPickerOpen(false)}
onOk={(p) => { form.setFieldsValue({ path_pattern: p }); setPathPickerOpen(false); }}
open={!!pathPickerField}
mode={pathPickerField === 'cron_path' ? 'any' : 'directory'}
initialPath={(pathPickerField === 'cron_path' ? watchedCronPath : watchedPathPrefix) || '/'}
onCancel={() => setPathPickerField(null)}
onOk={(p) => {
if (pathPickerField === 'cron_path') {
form.setFieldValue(['trigger_config', 'path'], p);
} else if (pathPickerField === 'path_prefix') {
form.setFieldValue(['trigger_config', 'path_prefix'], p);
}
setPathPickerField(null);
}}
/>
</PageCard>
);