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:
@@ -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
|
||||||
|
|||||||
@@ -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,26 +159,12 @@ 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'
|
name: name.trim(),
|
||||||
? discordChannelId.trim()
|
message: message.trim(),
|
||||||
: '';
|
schedule: finalSchedule,
|
||||||
|
enabled,
|
||||||
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,
|
|
||||||
});
|
|
||||||
onClose();
|
onClose();
|
||||||
toast.success(job ? t('toast.updated') : t('toast.created'));
|
toast.success(job ? t('toast.updated') : t('toast.created'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -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">
|
||||||
<span className="flex items-center gap-1">
|
{job.target && (
|
||||||
{CHANNEL_ICONS[job.target.channelType]}
|
<span className="flex items-center gap-1">
|
||||||
{job.target.channelName}
|
{CHANNEL_ICONS[job.target.channelType as ChannelType]}
|
||||||
</span>
|
{job.target.channelName}
|
||||||
|
</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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user