fix(cron): remove cron task channel config (#222)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Haze <hazeone@users.noreply.github.com>
This commit is contained in:
Haze
2026-02-28 15:14:02 +08:00
committed by GitHub
Unverified
parent e303841373
commit 4d948347ea
3 changed files with 27 additions and 109 deletions

View File

@@ -181,13 +181,11 @@ function transformCronJob(job: GatewayCronJob) {
// Extract message from payload // Extract message from payload
const message = job.payload?.message || job.payload?.text || ''; const message = job.payload?.message || job.payload?.text || '';
// Build target from delivery info // Build target from delivery info — only if a delivery channel is specified
const channelType = job.delivery?.channel || 'unknown'; const channelType = job.delivery?.channel;
const target = { const target = channelType
channelType, ? { channelType, channelId: channelType, channelName: channelType }
channelId: channelType, : undefined;
channelName: channelType,
};
// Build lastRun from state // Build lastRun from state
const lastRun = job.state?.lastRunAtMs const lastRun = job.state?.lastRunAtMs
@@ -241,21 +239,16 @@ function registerCronHandlers(gatewayManager: GatewayManager): void {
}); });
// Create a new cron job // 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: { ipcMain.handle('cron:create', async (_, input: {
name: string; name: string;
message: string; message: string;
schedule: string; schedule: string;
target: { channelType: string; channelId: string; channelName: string };
enabled?: boolean; enabled?: boolean;
}) => { }) => {
try { 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 = { const gatewayInput = {
name: input.name, name: input.name,
schedule: { kind: 'cron', expr: input.schedule }, schedule: { kind: 'cron', expr: input.schedule },
@@ -263,11 +256,6 @@ function registerCronHandlers(gatewayManager: GatewayManager): void {
enabled: input.enabled ?? true, enabled: input.enabled ?? true,
wakeMode: 'next-heartbeat', wakeMode: 'next-heartbeat',
sessionTarget: 'isolated', sessionTarget: 'isolated',
delivery: {
mode: 'announce',
channel: input.target.channelType,
to: deliveryTo,
},
}; };
const result = await gatewayManager.rpc('cron.add', gatewayInput); const result = await gatewayManager.rpc('cron.add', gatewayInput);
// Transform the returned job to frontend format // Transform the returned job to frontend format

View File

@@ -29,13 +29,12 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { useCronStore } from '@/stores/cron'; import { useCronStore } from '@/stores/cron';
import { useChannelsStore } from '@/stores/channels';
import { useGatewayStore } from '@/stores/gateway'; import { useGatewayStore } from '@/stores/gateway';
import { LoadingSpinner } from '@/components/common/LoadingSpinner'; import { LoadingSpinner } from '@/components/common/LoadingSpinner';
import { formatRelativeTime, cn } from '@/lib/utils'; import { formatRelativeTime, cn } from '@/lib/utils';
import { toast } from 'sonner'; import { toast } from 'sonner';
import type { CronJob, CronJobCreateInput, ScheduleType } from '@/types/cron'; 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'; import { useTranslation } from 'react-i18next';
// Common cron schedule presets // Common cron schedule presets
@@ -123,7 +122,6 @@ interface TaskDialogProps {
function TaskDialog({ job, onClose, onSave }: TaskDialogProps) { function TaskDialog({ job, onClose, onSave }: TaskDialogProps) {
const { t } = useTranslation('cron'); const { t } = useTranslation('cron');
const { channels } = useChannelsStore();
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [name, setName] = useState(job?.name || ''); const [name, setName] = useState(job?.name || '');
@@ -141,13 +139,8 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) {
const [schedule, setSchedule] = useState(initialSchedule); const [schedule, setSchedule] = useState(initialSchedule);
const [customSchedule, setCustomSchedule] = useState(''); const [customSchedule, setCustomSchedule] = useState('');
const [useCustom, setUseCustom] = useState(false); const [useCustom, setUseCustom] = useState(false);
const [channelId, setChannelId] = useState(job?.target.channelId || '');
const [discordChannelId, setDiscordChannelId] = useState('');
const [enabled, setEnabled] = useState(job?.enabled ?? true); const [enabled, setEnabled] = useState(job?.enabled ?? true);
const selectedChannel = channels.find((c) => c.id === channelId);
const isDiscord = selectedChannel?.type === 'discord';
const handleSubmit = async () => { const handleSubmit = async () => {
if (!name.trim()) { if (!name.trim()) {
toast.error(t('toast.nameRequired')); toast.error(t('toast.nameRequired'));
@@ -157,15 +150,6 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) {
toast.error(t('toast.messageRequired')); toast.error(t('toast.messageRequired'));
return; 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; const finalSchedule = useCustom ? customSchedule : schedule;
if (!finalSchedule.trim()) { if (!finalSchedule.trim()) {
@@ -175,24 +159,10 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) {
setSaving(true); setSaving(true);
try { try {
// For Discord, use the manually entered channel ID; for others, use empty await onSave({
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(), name: name.trim(),
message: message.trim(), message: message.trim(),
schedule: finalSchedule, schedule: finalSchedule,
target: {
channelType: selectedChannel!.type,
channelId: actualChannelId,
channelName: selectedChannel!.name,
},
enabled, enabled,
}); });
onClose(); onClose();
@@ -285,46 +255,6 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) {
</Button> </Button>
</div> </div>
{/* Target Channel */}
<div className="space-y-2">
<Label>{t('dialog.targetChannel')}</Label>
{channels.length === 0 ? (
<p className="text-sm text-muted-foreground">
{t('dialog.noChannels')}
</p>
) : (
<div className="grid grid-cols-2 gap-2">
{channels.map((channel) => (
<Button
key={channel.id}
type="button"
variant={channelId === channel.id ? 'default' : 'outline'}
size="sm"
onClick={() => setChannelId(channel.id)}
className="justify-start"
>
<span className="mr-2">{CHANNEL_ICONS[channel.type]}</span>
{channel.name}
</Button>
))}
</div>
)}
</div>
{/* Discord Channel ID - only shown when Discord is selected */}
{isDiscord && (
<div className="space-y-2">
<Label>{t('dialog.discordChannelId')}</Label>
<Input
value={discordChannelId}
onChange={(e) => setDiscordChannelId(e.target.value)}
placeholder={t('dialog.discordChannelIdPlaceholder')}
/>
<p className="text-xs text-muted-foreground">
{t('dialog.discordChannelIdDesc')}
</p>
</div>
)}
{/* Enabled */} {/* Enabled */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
@@ -442,10 +372,12 @@ function CronJobCard({ job, onToggle, onEdit, onDelete, onTrigger }: CronJobCard
{/* Metadata */} {/* Metadata */}
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-muted-foreground"> <div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-muted-foreground">
{job.target && (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
{CHANNEL_ICONS[job.target.channelType]} {CHANNEL_ICONS[job.target.channelType as ChannelType]}
{job.target.channelName} {job.target.channelName}
</span> </span>
)}
{job.lastRun && ( {job.lastRun && (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
@@ -507,20 +439,18 @@ function CronJobCard({ job, onToggle, onEdit, onDelete, onTrigger }: CronJobCard
export function Cron() { export function Cron() {
const { t } = useTranslation('cron'); const { t } = useTranslation('cron');
const { jobs, loading, error, fetchJobs, createJob, updateJob, toggleJob, deleteJob, triggerJob } = useCronStore(); const { jobs, loading, error, fetchJobs, createJob, updateJob, toggleJob, deleteJob, triggerJob } = useCronStore();
const { fetchChannels } = useChannelsStore();
const gatewayStatus = useGatewayStore((state) => state.status); const gatewayStatus = useGatewayStore((state) => state.status);
const [showDialog, setShowDialog] = useState(false); const [showDialog, setShowDialog] = useState(false);
const [editingJob, setEditingJob] = useState<CronJob | undefined>(); const [editingJob, setEditingJob] = useState<CronJob | undefined>();
const isGatewayRunning = gatewayStatus.state === 'running'; const isGatewayRunning = gatewayStatus.state === 'running';
// Fetch jobs and channels on mount // Fetch jobs on mount
useEffect(() => { useEffect(() => {
if (isGatewayRunning) { if (isGatewayRunning) {
fetchJobs(); fetchJobs();
fetchChannels();
} }
}, [fetchJobs, fetchChannels, isGatewayRunning]); }, [fetchJobs, isGatewayRunning]);
// Statistics // Statistics
const activeJobs = jobs.filter((j) => j.enabled); const activeJobs = jobs.filter((j) => j.enabled);

View File

@@ -41,7 +41,7 @@ export interface CronJob {
name: string; name: string;
message: string; message: string;
schedule: string | CronSchedule; schedule: string | CronSchedule;
target: CronJobTarget; target?: CronJobTarget;
enabled: boolean; enabled: boolean;
createdAt: string; createdAt: string;
updatedAt: 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 { export interface CronJobCreateInput {
name: string; name: string;
message: string; message: string;
schedule: string; schedule: string;
target: CronJobTarget;
enabled?: boolean; enabled?: boolean;
} }
@@ -67,7 +68,6 @@ export interface CronJobUpdateInput {
name?: string; name?: string;
message?: string; message?: string;
schedule?: string; schedule?: string;
target?: CronJobTarget;
enabled?: boolean; enabled?: boolean;
} }