Skip to content

Commit e6317ca

Browse files
authored
chore(frontend): fix corn task (#3)
1 parent fa6c23b commit e6317ca

12 files changed

Lines changed: 530 additions & 4399 deletions

File tree

ClawX-项目架构与版本大纲.md

Lines changed: 0 additions & 4296 deletions
This file was deleted.

electron/main/ipc-handlers.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ export function registerIpcHandlers(
7070

7171
// Skill config handlers (direct file access, no Gateway RPC)
7272
registerSkillConfigHandlers();
73+
74+
// Cron task handlers (proxy to Gateway RPC)
75+
registerCronHandlers(gatewayManager);
7376
}
7477

7578
/**
@@ -100,6 +103,183 @@ function registerSkillConfigHandlers(): void {
100103
});
101104
}
102105

106+
/**
107+
* Gateway CronJob type (as returned by cron.list RPC)
108+
*/
109+
interface GatewayCronJob {
110+
id: string;
111+
name: string;
112+
description?: string;
113+
enabled: boolean;
114+
createdAtMs: number;
115+
updatedAtMs: number;
116+
schedule: { kind: string; expr?: string; everyMs?: number; at?: string; tz?: string };
117+
payload: { kind: string; message?: string; text?: string };
118+
delivery?: { mode: string; channel?: string; to?: string };
119+
state: {
120+
nextRunAtMs?: number;
121+
lastRunAtMs?: number;
122+
lastStatus?: string;
123+
lastError?: string;
124+
lastDurationMs?: number;
125+
};
126+
}
127+
128+
/**
129+
* Transform a Gateway CronJob to the frontend CronJob format
130+
*/
131+
function transformCronJob(job: GatewayCronJob) {
132+
// Extract message from payload
133+
const message = job.payload?.message || job.payload?.text || '';
134+
135+
// Build target from delivery info
136+
const channelType = job.delivery?.channel || 'unknown';
137+
const target = {
138+
channelType,
139+
channelId: channelType,
140+
channelName: channelType,
141+
};
142+
143+
// Build lastRun from state
144+
const lastRun = job.state?.lastRunAtMs
145+
? {
146+
time: new Date(job.state.lastRunAtMs).toISOString(),
147+
success: job.state.lastStatus === 'ok',
148+
error: job.state.lastError,
149+
duration: job.state.lastDurationMs,
150+
}
151+
: undefined;
152+
153+
// Build nextRun from state
154+
const nextRun = job.state?.nextRunAtMs
155+
? new Date(job.state.nextRunAtMs).toISOString()
156+
: undefined;
157+
158+
return {
159+
id: job.id,
160+
name: job.name,
161+
message,
162+
schedule: job.schedule, // Pass the object through; frontend parseCronSchedule handles it
163+
target,
164+
enabled: job.enabled,
165+
createdAt: new Date(job.createdAtMs).toISOString(),
166+
updatedAt: new Date(job.updatedAtMs).toISOString(),
167+
lastRun,
168+
nextRun,
169+
};
170+
}
171+
172+
/**
173+
* Cron task IPC handlers
174+
* Proxies cron operations to the Gateway RPC service.
175+
* The frontend works with plain cron expression strings, but the Gateway
176+
* expects CronSchedule objects ({ kind: "cron", expr: "..." }).
177+
* These handlers bridge the two formats.
178+
*/
179+
function registerCronHandlers(gatewayManager: GatewayManager): void {
180+
// List all cron jobs — transforms Gateway CronJob format to frontend CronJob format
181+
ipcMain.handle('cron:list', async () => {
182+
try {
183+
const result = await gatewayManager.rpc('cron.list', { includeDisabled: true });
184+
const data = result as { jobs?: GatewayCronJob[] };
185+
const jobs = data?.jobs ?? [];
186+
// Transform Gateway format to frontend format
187+
return jobs.map(transformCronJob);
188+
} catch (error) {
189+
console.error('Failed to list cron jobs:', error);
190+
throw error;
191+
}
192+
});
193+
194+
// Create a new cron job
195+
ipcMain.handle('cron:create', async (_, input: {
196+
name: string;
197+
message: string;
198+
schedule: string;
199+
target: { channelType: string; channelId: string; channelName: string };
200+
enabled?: boolean;
201+
}) => {
202+
try {
203+
// Transform frontend input to Gateway cron.add format
204+
const gatewayInput = {
205+
name: input.name,
206+
schedule: { kind: 'cron', expr: input.schedule },
207+
payload: { kind: 'agentTurn', message: input.message },
208+
enabled: input.enabled ?? true,
209+
wakeMode: 'next-heartbeat',
210+
sessionTarget: 'isolated',
211+
delivery: {
212+
mode: 'announce',
213+
channel: input.target.channelType,
214+
},
215+
};
216+
const result = await gatewayManager.rpc('cron.add', gatewayInput);
217+
// Transform the returned job to frontend format
218+
if (result && typeof result === 'object') {
219+
return transformCronJob(result as GatewayCronJob);
220+
}
221+
return result;
222+
} catch (error) {
223+
console.error('Failed to create cron job:', error);
224+
throw error;
225+
}
226+
});
227+
228+
// Update an existing cron job
229+
ipcMain.handle('cron:update', async (_, id: string, input: Record<string, unknown>) => {
230+
try {
231+
// Transform schedule string to CronSchedule object if present
232+
const patch = { ...input };
233+
if (typeof patch.schedule === 'string') {
234+
patch.schedule = { kind: 'cron', expr: patch.schedule };
235+
}
236+
// Transform message to payload format if present
237+
if (typeof patch.message === 'string') {
238+
patch.payload = { kind: 'agentTurn', message: patch.message };
239+
delete patch.message;
240+
}
241+
const result = await gatewayManager.rpc('cron.update', { id, patch });
242+
return result;
243+
} catch (error) {
244+
console.error('Failed to update cron job:', error);
245+
throw error;
246+
}
247+
});
248+
249+
// Delete a cron job
250+
ipcMain.handle('cron:delete', async (_, id: string) => {
251+
try {
252+
const result = await gatewayManager.rpc('cron.remove', { id });
253+
return result;
254+
} catch (error) {
255+
console.error('Failed to delete cron job:', error);
256+
throw error;
257+
}
258+
});
259+
260+
// Toggle a cron job enabled/disabled
261+
ipcMain.handle('cron:toggle', async (_, id: string, enabled: boolean) => {
262+
try {
263+
const result = await gatewayManager.rpc('cron.update', { id, patch: { enabled } });
264+
return result;
265+
} catch (error) {
266+
console.error('Failed to toggle cron job:', error);
267+
throw error;
268+
}
269+
});
270+
271+
// Trigger a cron job manually
272+
ipcMain.handle('cron:trigger', async (_, id: string) => {
273+
try {
274+
const result = await gatewayManager.rpc('cron.run', { id, mode: 'force' });
275+
return result;
276+
} catch (error) {
277+
console.error('Failed to trigger cron job:', error);
278+
throw error;
279+
}
280+
});
281+
}
282+
103283
/**
104284
* UV-related IPC handlers
105285
*/

src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ function App() {
141141

142142
{/* Main application routes */}
143143
<Route element={<MainLayout />}>
144-
<Route path="/" element={<Dashboard />} />
145-
<Route path="/chat" element={<Chat />} />
144+
<Route path="/" element={<Chat />} />
145+
<Route path="/dashboard" element={<Dashboard />} />
146146
<Route path="/channels" element={<Channels />} />
147147
<Route path="/skills" element={<Skills />} />
148148
<Route path="/cron" element={<Cron />} />

src/components/layout/Header.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { ChatToolbar } from '@/pages/Chat/ChatToolbar';
88

99
// Page titles mapping
1010
const pageTitles: Record<string, string> = {
11-
'/': 'Dashboard',
12-
'/chat': 'Chat',
11+
'/': 'Chat',
12+
'/dashboard': 'Dashboard',
1313
'/channels': 'Channels',
1414
'/skills': 'Skills',
1515
'/cron': 'Cron Tasks',
@@ -19,7 +19,7 @@ const pageTitles: Record<string, string> = {
1919
export function Header() {
2020
const location = useLocation();
2121
const currentTitle = pageTitles[location.pathname] || 'ClawX';
22-
const isChatPage = location.pathname === '/chat';
22+
const isChatPage = location.pathname === '/';
2323

2424
return (
2525
<header className="flex h-14 items-center justify-between border-b bg-background px-6">

src/components/layout/Sidebar.tsx

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
} from 'lucide-react';
1818
import { cn } from '@/lib/utils';
1919
import { useSettingsStore } from '@/stores/settings';
20-
import { useGatewayStore } from '@/stores/gateway';
20+
2121
import { Button } from '@/components/ui/button';
2222
import { Badge } from '@/components/ui/badge';
2323

@@ -64,19 +64,17 @@ export function Sidebar() {
6464
const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
6565
const setSidebarCollapsed = useSettingsStore((state) => state.setSidebarCollapsed);
6666
const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked);
67-
const gatewayStatus = useGatewayStore((state) => state.status);
68-
6967
// Open developer console
7068
const openDevConsole = () => {
7169
window.electron.openExternal('http://localhost:18789');
7270
};
7371

7472
const navItems = [
75-
{ to: '/', icon: <Home className="h-5 w-5" />, label: 'Dashboard' },
76-
{ to: '/chat', icon: <MessageSquare className="h-5 w-5" />, label: 'Chat' },
77-
{ to: '/channels', icon: <Radio className="h-5 w-5" />, label: 'Channels' },
78-
{ to: '/skills', icon: <Puzzle className="h-5 w-5" />, label: 'Skills' },
73+
{ to: '/', icon: <MessageSquare className="h-5 w-5" />, label: 'Chat' },
7974
{ to: '/cron', icon: <Clock className="h-5 w-5" />, label: 'Cron Tasks' },
75+
{ to: '/skills', icon: <Puzzle className="h-5 w-5" />, label: 'Skills' },
76+
{ to: '/channels', icon: <Radio className="h-5 w-5" />, label: 'Channels' },
77+
{ to: '/dashboard', icon: <Home className="h-5 w-5" />, label: 'Dashboard' },
8078
{ to: '/settings', icon: <Settings className="h-5 w-5" />, label: 'Settings' },
8179
];
8280

@@ -108,30 +106,7 @@ export function Sidebar() {
108106
</nav>
109107

110108
{/* Footer */}
111-
<div className="border-t p-2 space-y-2">
112-
{/* Gateway Status */}
113-
<div
114-
className={cn(
115-
'flex items-center gap-2 rounded-lg px-3 py-2',
116-
sidebarCollapsed && 'justify-center px-2'
117-
)}
118-
>
119-
<div
120-
className={cn(
121-
'h-2 w-2 rounded-full',
122-
gatewayStatus.state === 'running' && 'bg-green-500',
123-
gatewayStatus.state === 'starting' && 'bg-yellow-500 animate-pulse',
124-
gatewayStatus.state === 'stopped' && 'bg-gray-400',
125-
gatewayStatus.state === 'error' && 'bg-red-500'
126-
)}
127-
/>
128-
{!sidebarCollapsed && (
129-
<span className="text-xs text-muted-foreground">
130-
Gateway: {gatewayStatus.state}
131-
</span>
132-
)}
133-
</div>
134-
109+
<div className="p-2 space-y-2">
135110
{/* Developer Mode Button */}
136111
{devModeUnlocked && !sidebarCollapsed && (
137112
<Button

0 commit comments

Comments
 (0)