Files
beaver_project/app-instance/frontend/app/(app)/cron/page.tsx
steven_li 29dfd14aa6 ```
feat(agent): 添加对持久化子智能体的支持并增强委派管理

添加了持久化子智能体的完整生命周期管理功能,包括创建、更新、删除和查询API接口。
新增了子智能体的JSON-RPC通信协议支持,实现了远程调用和任务管理功能。

同时增强了委派管理器的功能:
- 添加了对本地委派、插件委派和本地回退的开关控制
- 实现了持久化子智能体任务的自动检测和本地执行保护
- 增加了对不同委派类型的权限验证机制

修改了智能体注册表以支持插件智能体的条件性包含,并更新了工具注册逻辑以支持可选工具。

BREAKING CHANGE: 委派管理器的构造函数签名已更改,添加了新的控制参数。
```
2026-03-27 10:15:35 +08:00

416 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React, { useEffect, useState } from 'react';
import {
Clock,
Plus,
Trash2,
Play,
RefreshCw,
Loader2,
AlertCircle,
X,
} from 'lucide-react';
import {
listCronJobs,
addCronJob,
removeCronJob,
toggleCronJob,
runCronJob,
} from '@/lib/api';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { TaskManagementTabs } from '@/components/task-management/TaskManagementTabs';
import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useChatStore } from '@/lib/store';
import type { CronJob } from '@/types';
export default function CronPage() {
const sessionId = useChatStore((s) => s.sessionId);
const [jobs, setJobs] = useState<CronJob[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showAdd, setShowAdd] = useState(false);
const targetSessionKey = sessionId.startsWith('web:') ? sessionId : 'web:default';
const loadJobs = async () => {
setLoading(true);
setError(null);
try {
const data = await listCronJobs(true);
setJobs(data);
} catch (err: any) {
setError(err.message || '加载任务失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadJobs();
}, []);
const handleToggle = async (jobId: string, enabled: boolean) => {
try {
await toggleCronJob(jobId, enabled);
loadJobs();
} catch {
// ignore
}
};
const handleDelete = async (jobId: string) => {
try {
await removeCronJob(jobId);
loadJobs();
} catch {
// ignore
}
};
const handleRun = async (jobId: string) => {
try {
await runCronJob(jobId);
loadJobs();
} catch {
// ignore
}
};
const handleAdd = async (params: {
name: string;
message: string;
every_seconds?: number;
cron_expr?: string;
}) => {
try {
await addCronJob({
...params,
session_key: targetSessionKey,
});
setShowAdd(false);
loadJobs();
} catch (err: any) {
setError(err.message);
}
};
const formatTime = (ms: number | null) => {
if (!ms) return '-';
return new Date(ms).toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="max-w-5xl mx-auto p-6 space-y-6">
<TaskManagementTabs />
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold flex items-center gap-2">
<Clock className="w-6 h-6" />
</h1>
<div className="flex items-center gap-2">
<Button onClick={loadJobs} variant="outline" size="sm">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
<Button onClick={() => setShowAdd(true)} size="sm">
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
{error && (
<Card className="border-destructive">
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-destructive text-sm">
<AlertCircle className="w-4 h-4" />
{error}
</div>
</CardContent>
</Card>
)}
{/* Add Job Form */}
{showAdd && (
<AddJobForm
targetSessionKey={targetSessionKey}
onAdd={handleAdd}
onCancel={() => setShowAdd(false)}
/>
)}
{/* Jobs Table */}
<Card>
<CardContent className="p-0">
{jobs.length === 0 ? (
<div className="py-12 text-center text-muted-foreground">
<Clock className="w-10 h-10 mx-auto mb-3 opacity-30" />
<p className="font-medium"></p>
<p className="text-sm mt-1"></p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-16"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="w-24"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{jobs.map((job) => (
<TableRow key={job.id}>
<TableCell>
<Switch
checked={job.enabled}
onCheckedChange={(checked) =>
handleToggle(job.id, checked)
}
/>
</TableCell>
<TableCell className="font-medium">
<div>
<span>{job.name}</span>
<span className="text-xs text-muted-foreground ml-2">
{job.id}
</span>
</div>
</TableCell>
<TableCell>
<code className="text-xs bg-muted px-1.5 py-0.5 rounded">
{job.schedule_display}
</code>
</TableCell>
<TableCell>
<span className="text-sm truncate max-w-[200px] block">
{job.message}
</span>
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{formatTime(job.last_run_at_ms)}
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{formatTime(job.next_run_at_ms)}
</TableCell>
<TableCell>
{job.last_status === 'ok' && (
<Badge variant="default" className="text-xs bg-green-600">
OK
</Badge>
)}
{job.last_status === 'error' && (
<Badge variant="destructive" className="text-xs">
</Badge>
)}
{!job.last_status && (
<span className="text-xs text-muted-foreground">
-
</span>
)}
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => handleRun(job.id)}
title="立即执行"
>
<Play className="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive"
onClick={() => handleDelete(job.id)}
title="删除"
>
<Trash2 className="w-3.5 h-3.5" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
);
}
function AddJobForm({
targetSessionKey,
onAdd,
onCancel,
}: {
targetSessionKey: string;
onAdd: (params: {
name: string;
message: string;
every_seconds?: number;
cron_expr?: string;
}) => void;
onCancel: () => void;
}) {
const [name, setName] = useState('');
const [message, setMessage] = useState('');
const [scheduleType, setScheduleType] = useState<'every' | 'cron'>('every');
const [everySeconds, setEverySeconds] = useState('3600');
const [cronExpr, setCronExpr] = useState('0 9 * * *');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim() || !message.trim()) return;
const params: any = { name: name.trim(), message: message.trim() };
if (scheduleType === 'every') {
params.every_seconds = parseInt(everySeconds, 10) || 3600;
} else {
params.cron_expr = cronExpr.trim();
}
onAdd(params);
};
return (
<Card>
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
<CardTitle className="text-base"></CardTitle>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={onCancel}>
<X className="w-4 h-4" />
</Button>
</div>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name"></Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="例如:日报汇总"
/>
</div>
<div className="space-y-2">
<Label htmlFor="schedule-type"></Label>
<Select
value={scheduleType}
onValueChange={(v) => setScheduleType(v as 'every' | 'cron')}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="every"> N </SelectItem>
<SelectItem value="cron">Cron </SelectItem>
</SelectContent>
</Select>
</div>
</div>
{scheduleType === 'every' ? (
<div className="space-y-2">
<Label htmlFor="every"></Label>
<Input
id="every"
type="number"
value={everySeconds}
onChange={(e) => setEverySeconds(e.target.value)}
min="10"
placeholder="3600"
/>
<p className="text-xs text-muted-foreground">
{parseInt(everySeconds, 10) >= 3600
? `${Math.floor(parseInt(everySeconds, 10) / 3600)} 小时 ${Math.floor((parseInt(everySeconds, 10) % 3600) / 60)}`
: parseInt(everySeconds, 10) >= 60
? `${Math.floor(parseInt(everySeconds, 10) / 60)}${parseInt(everySeconds, 10) % 60}`
: ''}
</p>
</div>
) : (
<div className="space-y-2">
<Label htmlFor="cron">Cron </Label>
<Input
id="cron"
value={cronExpr}
onChange={(e) => setCronExpr(e.target.value)}
placeholder="0 9 * * *"
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
)}
<div className="space-y-2">
<Label htmlFor="message"></Label>
<Input
id="message"
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="例如:检查我的邮件并生成摘要"
/>
<p className="text-xs text-muted-foreground">
Web <code className="bg-muted px-1 py-0.5 rounded">{targetSessionKey}</code>
</p>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={onCancel}>
</Button>
<Button type="submit" disabled={!name.trim() || !message.trim()}>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</form>
</CardContent>
</Card>
);
}