Refactor clawx (#344)

Co-authored-by: ashione <skyzlxuan@gmail.com>
This commit is contained in:
paisley
2026-03-09 13:10:42 +08:00
committed by GitHub
Unverified
parent 3d804a9f5e
commit 2c5c82bb74
75 changed files with 7640 additions and 3106 deletions

168
electron/api/routes/cron.ts Normal file
View File

@@ -0,0 +1,168 @@
import type { IncomingMessage, ServerResponse } from 'http';
import type { HostApiContext } from '../context';
import { parseJsonBody, sendJson } from '../route-utils';
interface GatewayCronJob {
id: string;
name: string;
description?: string;
enabled: boolean;
createdAtMs: number;
updatedAtMs: number;
schedule: { kind: string; expr?: string; everyMs?: number; at?: string; tz?: string };
payload: { kind: string; message?: string; text?: string };
delivery?: { mode: string; channel?: string; to?: string };
sessionTarget?: string;
state: {
nextRunAtMs?: number;
lastRunAtMs?: number;
lastStatus?: string;
lastError?: string;
lastDurationMs?: number;
};
}
function transformCronJob(job: GatewayCronJob) {
const message = job.payload?.message || job.payload?.text || '';
const channelType = job.delivery?.channel;
const target = channelType
? { channelType, channelId: channelType, channelName: channelType }
: undefined;
const lastRun = job.state?.lastRunAtMs
? {
time: new Date(job.state.lastRunAtMs).toISOString(),
success: job.state.lastStatus === 'ok',
error: job.state.lastError,
duration: job.state.lastDurationMs,
}
: undefined;
const nextRun = job.state?.nextRunAtMs
? new Date(job.state.nextRunAtMs).toISOString()
: undefined;
return {
id: job.id,
name: job.name,
message,
schedule: job.schedule,
target,
enabled: job.enabled,
createdAt: new Date(job.createdAtMs).toISOString(),
updatedAt: new Date(job.updatedAtMs).toISOString(),
lastRun,
nextRun,
};
}
export async function handleCronRoutes(
req: IncomingMessage,
res: ServerResponse,
url: URL,
ctx: HostApiContext,
): Promise<boolean> {
if (url.pathname === '/api/cron/jobs' && req.method === 'GET') {
try {
const result = await ctx.gatewayManager.rpc('cron.list', { includeDisabled: true });
const data = result as { jobs?: GatewayCronJob[] };
const jobs = data?.jobs ?? [];
for (const job of jobs) {
const isIsolatedAgent =
(job.sessionTarget === 'isolated' || !job.sessionTarget) &&
job.payload?.kind === 'agentTurn';
const needsRepair =
isIsolatedAgent &&
job.delivery?.mode === 'announce' &&
!job.delivery?.channel;
if (needsRepair) {
try {
await ctx.gatewayManager.rpc('cron.update', {
id: job.id,
patch: { delivery: { mode: 'none' } },
});
job.delivery = { mode: 'none' };
if (job.state?.lastError?.includes('Channel is required')) {
job.state.lastError = undefined;
job.state.lastStatus = 'ok';
}
} catch {
// ignore per-job repair failure
}
}
}
sendJson(res, 200, jobs.map(transformCronJob));
} catch (error) {
sendJson(res, 500, { success: false, error: String(error) });
}
return true;
}
if (url.pathname === '/api/cron/jobs' && req.method === 'POST') {
try {
const input = await parseJsonBody<{ name: string; message: string; schedule: string; enabled?: boolean }>(req);
const result = await ctx.gatewayManager.rpc('cron.add', {
name: input.name,
schedule: { kind: 'cron', expr: input.schedule },
payload: { kind: 'agentTurn', message: input.message },
enabled: input.enabled ?? true,
wakeMode: 'next-heartbeat',
sessionTarget: 'isolated',
delivery: { mode: 'none' },
});
sendJson(res, 200, result && typeof result === 'object' ? transformCronJob(result as GatewayCronJob) : result);
} catch (error) {
sendJson(res, 500, { success: false, error: String(error) });
}
return true;
}
if (url.pathname.startsWith('/api/cron/jobs/') && req.method === 'PUT') {
try {
const id = decodeURIComponent(url.pathname.slice('/api/cron/jobs/'.length));
const input = await parseJsonBody<Record<string, unknown>>(req);
const patch = { ...input };
if (typeof patch.schedule === 'string') {
patch.schedule = { kind: 'cron', expr: patch.schedule };
}
if (typeof patch.message === 'string') {
patch.payload = { kind: 'agentTurn', message: patch.message };
delete patch.message;
}
sendJson(res, 200, await ctx.gatewayManager.rpc('cron.update', { id, patch }));
} catch (error) {
sendJson(res, 500, { success: false, error: String(error) });
}
return true;
}
if (url.pathname.startsWith('/api/cron/jobs/') && req.method === 'DELETE') {
try {
const id = decodeURIComponent(url.pathname.slice('/api/cron/jobs/'.length));
sendJson(res, 200, await ctx.gatewayManager.rpc('cron.remove', { id }));
} catch (error) {
sendJson(res, 500, { success: false, error: String(error) });
}
return true;
}
if (url.pathname === '/api/cron/toggle' && req.method === 'POST') {
try {
const body = await parseJsonBody<{ id: string; enabled: boolean }>(req);
sendJson(res, 200, await ctx.gatewayManager.rpc('cron.update', { id: body.id, patch: { enabled: body.enabled } }));
} catch (error) {
sendJson(res, 500, { success: false, error: String(error) });
}
return true;
}
if (url.pathname === '/api/cron/trigger' && req.method === 'POST') {
try {
const body = await parseJsonBody<{ id: string }>(req);
sendJson(res, 200, await ctx.gatewayManager.rpc('cron.run', { id: body.id, mode: 'force' }));
} catch (error) {
sendJson(res, 500, { success: false, error: String(error) });
}
return true;
}
return false;
}