From 4d948347ea0b2e1465f613b598dfb565a74ed3e1 Mon Sep 17 00:00:00 2001 From: Haze <709547807@qq.com> Date: Sat, 28 Feb 2026 15:14:02 +0800 Subject: [PATCH] fix(cron): remove cron task channel config (#222) Co-authored-by: Cursor Agent Co-authored-by: Haze --- electron/main/ipc-handlers.ts | 28 +++------- src/pages/Cron/index.tsx | 100 +++++----------------------------- src/types/cron.ts | 8 +-- 3 files changed, 27 insertions(+), 109 deletions(-) diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index 29e6c26e1..508fe5861 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -181,13 +181,11 @@ function transformCronJob(job: GatewayCronJob) { // Extract message from payload const message = job.payload?.message || job.payload?.text || ''; - // Build target from delivery info - const channelType = job.delivery?.channel || 'unknown'; - const target = { - channelType, - channelId: channelType, - channelName: channelType, - }; + // Build target from delivery info — only if a delivery channel is specified + const channelType = job.delivery?.channel; + const target = channelType + ? { channelType, channelId: channelType, channelName: channelType } + : undefined; // Build lastRun from state const lastRun = job.state?.lastRunAtMs @@ -241,21 +239,16 @@ function registerCronHandlers(gatewayManager: GatewayManager): void { }); // Create a new cron job + // UI-created tasks have no delivery target — results go to the ClawX chat page. + // Tasks created via external channels (Feishu, Discord, etc.) are handled + // directly by the OpenClaw Gateway and do not pass through this IPC handler. ipcMain.handle('cron:create', async (_, input: { name: string; message: string; schedule: string; - target: { channelType: string; channelId: string; channelName: string }; enabled?: boolean; }) => { try { - // Transform frontend input to Gateway cron.add format - // For Discord, the recipient must be prefixed with "channel:" or "user:" - const recipientId = input.target.channelId; - const deliveryTo = input.target.channelType === 'discord' && recipientId - ? `channel:${recipientId}` - : recipientId; - const gatewayInput = { name: input.name, schedule: { kind: 'cron', expr: input.schedule }, @@ -263,11 +256,6 @@ function registerCronHandlers(gatewayManager: GatewayManager): void { enabled: input.enabled ?? true, wakeMode: 'next-heartbeat', sessionTarget: 'isolated', - delivery: { - mode: 'announce', - channel: input.target.channelType, - to: deliveryTo, - }, }; const result = await gatewayManager.rpc('cron.add', gatewayInput); // Transform the returned job to frontend format diff --git a/src/pages/Cron/index.tsx b/src/pages/Cron/index.tsx index 983be5262..d2604ed59 100644 --- a/src/pages/Cron/index.tsx +++ b/src/pages/Cron/index.tsx @@ -29,13 +29,12 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { useCronStore } from '@/stores/cron'; -import { useChannelsStore } from '@/stores/channels'; import { useGatewayStore } from '@/stores/gateway'; import { LoadingSpinner } from '@/components/common/LoadingSpinner'; import { formatRelativeTime, cn } from '@/lib/utils'; import { toast } from 'sonner'; import type { CronJob, CronJobCreateInput, ScheduleType } from '@/types/cron'; -import { CHANNEL_ICONS } from '@/types/channel'; +import { CHANNEL_ICONS, type ChannelType } from '@/types/channel'; import { useTranslation } from 'react-i18next'; // Common cron schedule presets @@ -123,7 +122,6 @@ interface TaskDialogProps { function TaskDialog({ job, onClose, onSave }: TaskDialogProps) { const { t } = useTranslation('cron'); - const { channels } = useChannelsStore(); const [saving, setSaving] = useState(false); const [name, setName] = useState(job?.name || ''); @@ -141,13 +139,8 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) { const [schedule, setSchedule] = useState(initialSchedule); const [customSchedule, setCustomSchedule] = useState(''); const [useCustom, setUseCustom] = useState(false); - const [channelId, setChannelId] = useState(job?.target.channelId || ''); - const [discordChannelId, setDiscordChannelId] = useState(''); const [enabled, setEnabled] = useState(job?.enabled ?? true); - const selectedChannel = channels.find((c) => c.id === channelId); - const isDiscord = selectedChannel?.type === 'discord'; - const handleSubmit = async () => { if (!name.trim()) { toast.error(t('toast.nameRequired')); @@ -157,15 +150,6 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) { toast.error(t('toast.messageRequired')); return; } - if (!channelId) { - toast.error(t('toast.channelRequired')); - return; - } - // Validate Discord channel ID when Discord is selected - if (selectedChannel?.type === 'discord' && !discordChannelId.trim()) { - toast.error(t('toast.discordIdRequired')); - return; - } const finalSchedule = useCustom ? customSchedule : schedule; if (!finalSchedule.trim()) { @@ -175,26 +159,12 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) { setSaving(true); try { - // For Discord, use the manually entered channel ID; for others, use empty - const actualChannelId = selectedChannel!.type === 'discord' - ? discordChannelId.trim() - : ''; - - await onSave( - // ... (args omitted from replacement content, ensuring they match target if not changed, but here I am replacing the block) - // Wait, I should not replace the whole onSave call if I don't need to. - // Let's target the toast. - { - name: name.trim(), - message: message.trim(), - schedule: finalSchedule, - target: { - channelType: selectedChannel!.type, - channelId: actualChannelId, - channelName: selectedChannel!.name, - }, - enabled, - }); + await onSave({ + name: name.trim(), + message: message.trim(), + schedule: finalSchedule, + enabled, + }); onClose(); toast.success(job ? t('toast.updated') : t('toast.created')); } catch (err) { @@ -285,46 +255,6 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) { - {/* Target Channel */} -
- - {channels.length === 0 ? ( -

- {t('dialog.noChannels')} -

- ) : ( -
- {channels.map((channel) => ( - - ))} -
- )} -
- - {/* Discord Channel ID - only shown when Discord is selected */} - {isDiscord && ( -
- - setDiscordChannelId(e.target.value)} - placeholder={t('dialog.discordChannelIdPlaceholder')} - /> -

- {t('dialog.discordChannelIdDesc')} -

-
- )} {/* Enabled */}
@@ -442,10 +372,12 @@ function CronJobCard({ job, onToggle, onEdit, onDelete, onTrigger }: CronJobCard {/* Metadata */}
- - {CHANNEL_ICONS[job.target.channelType]} - {job.target.channelName} - + {job.target && ( + + {CHANNEL_ICONS[job.target.channelType as ChannelType]} + {job.target.channelName} + + )} {job.lastRun && ( @@ -507,20 +439,18 @@ function CronJobCard({ job, onToggle, onEdit, onDelete, onTrigger }: CronJobCard export function Cron() { const { t } = useTranslation('cron'); const { jobs, loading, error, fetchJobs, createJob, updateJob, toggleJob, deleteJob, triggerJob } = useCronStore(); - const { fetchChannels } = useChannelsStore(); const gatewayStatus = useGatewayStore((state) => state.status); const [showDialog, setShowDialog] = useState(false); const [editingJob, setEditingJob] = useState(); const isGatewayRunning = gatewayStatus.state === 'running'; - // Fetch jobs and channels on mount + // Fetch jobs on mount useEffect(() => { if (isGatewayRunning) { fetchJobs(); - fetchChannels(); } - }, [fetchJobs, fetchChannels, isGatewayRunning]); + }, [fetchJobs, isGatewayRunning]); // Statistics const activeJobs = jobs.filter((j) => j.enabled); diff --git a/src/types/cron.ts b/src/types/cron.ts index 78488a382..0558e25d8 100644 --- a/src/types/cron.ts +++ b/src/types/cron.ts @@ -41,7 +41,7 @@ export interface CronJob { name: string; message: string; schedule: string | CronSchedule; - target: CronJobTarget; + target?: CronJobTarget; enabled: boolean; createdAt: string; updatedAt: string; @@ -50,13 +50,14 @@ export interface CronJob { } /** - * Input for creating a cron job + * Input for creating a cron job from the UI. + * No target/delivery — UI-created tasks push results to the ClawX chat page. + * Tasks created via external channels are handled directly by the Gateway. */ export interface CronJobCreateInput { name: string; message: string; schedule: string; - target: CronJobTarget; enabled?: boolean; } @@ -67,7 +68,6 @@ export interface CronJobUpdateInput { name?: string; message?: string; schedule?: string; - target?: CronJobTarget; enabled?: boolean; }