feat: 任务页新增定时计划——指定执行智能体(默认通用助手)与间隔,后台到点自动在智能体会话中执行并记录结果
This commit is contained in:
@@ -43,6 +43,7 @@ export default function App() {
|
||||
const refreshConversations = useStore((s) => s.refreshConversations);
|
||||
const refreshAgents = useStore((s) => s.refreshAgents);
|
||||
const refreshWorkflows = useStore((s) => s.refreshWorkflows);
|
||||
const refreshScheduledTasks = useStore((s) => s.refreshScheduledTasks);
|
||||
const setDeployState = useStore((s) => s.setDeployState);
|
||||
const setDeployProgress = useStore((s) => s.setDeployProgress);
|
||||
const setChatSuggestions = useStore((s) => s.setChatSuggestions);
|
||||
@@ -65,6 +66,7 @@ export default function App() {
|
||||
refreshConversations();
|
||||
refreshAgents();
|
||||
refreshWorkflows();
|
||||
refreshScheduledTasks();
|
||||
const un1 = onEvent("engine://status", () => refreshEngine());
|
||||
const un2 = onEvent("server://status", () => refreshServer());
|
||||
const un3start = onEvent<DownloadProgressEvent>("download://started", (e) => {
|
||||
@@ -152,6 +154,8 @@ export default function App() {
|
||||
const un7 = onEvent<ChatSuggestionsEvent>("chat://suggestions", (e) => {
|
||||
setChatSuggestions(e.message_id, e.suggestions);
|
||||
});
|
||||
const un8 = onEvent("scheduled://updated", () => refreshScheduledTasks());
|
||||
const un9 = onEvent("conversations://updated", () => refreshConversations());
|
||||
return () => {
|
||||
un1.then((f) => f());
|
||||
un2.then((f) => f());
|
||||
@@ -163,6 +167,8 @@ export default function App() {
|
||||
un5.then((f) => f());
|
||||
un6.then((f) => f());
|
||||
un7.then((f) => f());
|
||||
un8.then((f) => f());
|
||||
un9.then((f) => f());
|
||||
};
|
||||
}, [
|
||||
refreshModels,
|
||||
@@ -171,6 +177,7 @@ export default function App() {
|
||||
refreshConversations,
|
||||
refreshAgents,
|
||||
refreshWorkflows,
|
||||
refreshScheduledTasks,
|
||||
setDeployState,
|
||||
setDeployProgress,
|
||||
setChatSuggestions,
|
||||
|
||||
@@ -160,6 +160,30 @@ export interface WorkflowNodeStatusEvent {
|
||||
text: string | null;
|
||||
}
|
||||
|
||||
export interface ScheduledTask {
|
||||
id: string;
|
||||
name: string;
|
||||
agent_id: string | null;
|
||||
prompt: string;
|
||||
interval_minutes: number;
|
||||
enabled: boolean;
|
||||
next_run_at: string | null;
|
||||
last_run_at: string | null;
|
||||
last_status: string;
|
||||
last_result: string | null;
|
||||
last_error: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ScheduledTaskInput {
|
||||
name: string;
|
||||
agent_id: string | null;
|
||||
prompt: string;
|
||||
interval_minutes: number;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface McpServer {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -363,6 +387,17 @@ export const api = {
|
||||
input: string;
|
||||
model_id: string | null;
|
||||
}) => invoke<WorkflowRunResult>("run_workflow", { payload }),
|
||||
listScheduledTasks: () => invoke<ScheduledTask[]>("list_scheduled_tasks"),
|
||||
addScheduledTask: (input: ScheduledTaskInput) =>
|
||||
invoke<ScheduledTask>("add_scheduled_task", { input }),
|
||||
updateScheduledTask: (id: string, input: ScheduledTaskInput) =>
|
||||
invoke<void>("update_scheduled_task", { id, input }),
|
||||
removeScheduledTask: (id: string) =>
|
||||
invoke<void>("remove_scheduled_task", { id }),
|
||||
setScheduledTaskEnabled: (id: string, enabled: boolean) =>
|
||||
invoke<void>("set_scheduled_task_enabled", { id, enabled }),
|
||||
runScheduledTaskNow: (id: string) =>
|
||||
invoke<void>("run_scheduled_task_now", { id }),
|
||||
listModels: () => invoke<ModelInfo[]>("list_models"),
|
||||
importModel: (path: string) => invoke<ModelInfo>("import_model", { path }),
|
||||
removeModel: (id: string) => invoke<void>("remove_model", { id }),
|
||||
|
||||
@@ -103,6 +103,25 @@ const paths: Record<string, ReactNode> = {
|
||||
paperclip: (
|
||||
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
|
||||
),
|
||||
clock: (
|
||||
<>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 6v6l4 2" />
|
||||
</>
|
||||
),
|
||||
calendar: (
|
||||
<>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||
<path d="M16 2v4M8 2v4M3 10h18" />
|
||||
</>
|
||||
),
|
||||
refresh: (
|
||||
<>
|
||||
<path d="M23 4v6h-6M1 20v-6h6" />
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
||||
</>
|
||||
),
|
||||
play: <polygon points="5 3 19 12 5 21 5 3" />,
|
||||
};
|
||||
|
||||
export default function Icon({
|
||||
|
||||
+528
-17
@@ -1,41 +1,552 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { api, ScheduledTask, ScheduledTaskInput } from "../api";
|
||||
import { useStore, TaskItem } from "../store";
|
||||
import Icon from "../components/Icon";
|
||||
|
||||
const PRESET_GENERAL_ASSISTANT = "preset-general-assistant";
|
||||
|
||||
const INTERVAL_PRESETS: { label: string; value: number; unit: IntervalUnit }[] = [
|
||||
{ label: "30 分钟", value: 30, unit: "min" },
|
||||
{ label: "1 小时", value: 1, unit: "hour" },
|
||||
{ label: "6 小时", value: 6, unit: "hour" },
|
||||
{ label: "1 天", value: 1, unit: "day" },
|
||||
];
|
||||
|
||||
type IntervalUnit = "min" | "hour" | "day";
|
||||
|
||||
interface Draft {
|
||||
name: string;
|
||||
agentId: string;
|
||||
prompt: string;
|
||||
intervalValue: number;
|
||||
intervalUnit: IntervalUnit;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
function intervalToMinutes(value: number, unit: IntervalUnit) {
|
||||
const factor = unit === "hour" ? 60 : unit === "day" ? 1440 : 1;
|
||||
return Math.max(1, Math.round(value * factor));
|
||||
}
|
||||
|
||||
function fmtInterval(minutes: number) {
|
||||
if (minutes % 1440 === 0) {
|
||||
const days = minutes / 1440;
|
||||
return days === 1 ? "每天" : `每 ${days} 天`;
|
||||
}
|
||||
if (minutes % 60 === 0) {
|
||||
const hours = minutes / 60;
|
||||
return hours === 1 ? "每小时" : `每 ${hours} 小时`;
|
||||
}
|
||||
return `每 ${minutes} 分钟`;
|
||||
}
|
||||
|
||||
function fmtDbTime(s: string | null) {
|
||||
if (!s) return "—";
|
||||
const d = new Date(s.replace(" ", "T") + "Z");
|
||||
if (Number.isNaN(d.getTime())) return s;
|
||||
return d.toLocaleString("zh-CN", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
}
|
||||
|
||||
export default function TasksPage() {
|
||||
const tasks = useStore((s) => s.tasks);
|
||||
const clearFinishedTasks = useStore((s) => s.clearFinishedTasks);
|
||||
const sorted = [...tasks].sort((a, b) => b.updated_at - a.updated_at);
|
||||
const scheduledTasks = useStore((s) => s.scheduledTasks);
|
||||
const refreshScheduledTasks = useStore((s) => s.refreshScheduledTasks);
|
||||
const agents = useStore((s) => s.agents);
|
||||
const refreshAgents = useStore((s) => s.refreshAgents);
|
||||
const [msg, setMsg] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [editing, setEditing] = useState<ScheduledTask | null>(null);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
refreshScheduledTasks();
|
||||
refreshAgents();
|
||||
}, [refreshScheduledTasks, refreshAgents]);
|
||||
|
||||
const sorted = useMemo(
|
||||
() => [...tasks].sort((a, b) => b.updated_at - a.updated_at),
|
||||
[tasks],
|
||||
);
|
||||
const hasFinished = tasks.some((t) => t.status !== "running");
|
||||
const agentNameById = useMemo(() => {
|
||||
const map = new Map<string, { name: string; icon: string }>();
|
||||
for (const a of agents) map.set(a.id, { name: a.name, icon: a.icon });
|
||||
return map;
|
||||
}, [agents]);
|
||||
|
||||
const defaultAgentId = useMemo(() => {
|
||||
if (agents.some((a) => a.id === PRESET_GENERAL_ASSISTANT)) {
|
||||
return PRESET_GENERAL_ASSISTANT;
|
||||
}
|
||||
return agents[0]?.id ?? "";
|
||||
}, [agents]);
|
||||
|
||||
function openNew() {
|
||||
setEditing(null);
|
||||
setShowModal(true);
|
||||
}
|
||||
|
||||
function openEdit(task: ScheduledTask) {
|
||||
setEditing(task);
|
||||
setShowModal(true);
|
||||
}
|
||||
|
||||
async function handleSave(draft: Draft) {
|
||||
const intervalMinutes = intervalToMinutes(draft.intervalValue, draft.intervalUnit);
|
||||
const input: ScheduledTaskInput = {
|
||||
name: draft.name.trim(),
|
||||
agent_id: draft.agentId || null,
|
||||
prompt: draft.prompt.trim(),
|
||||
interval_minutes: intervalMinutes,
|
||||
enabled: draft.enabled,
|
||||
};
|
||||
setBusy(true);
|
||||
try {
|
||||
if (editing) {
|
||||
await api.updateScheduledTask(editing.id, input);
|
||||
setMsg("定时计划已更新");
|
||||
} else {
|
||||
await api.addScheduledTask(input);
|
||||
setMsg("定时计划已创建");
|
||||
}
|
||||
await refreshScheduledTasks();
|
||||
setShowModal(false);
|
||||
} catch (e) {
|
||||
setMsg(`保存失败:${String(e)}`);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRunNow(task: ScheduledTask) {
|
||||
setBusy(true);
|
||||
try {
|
||||
await api.runScheduledTaskNow(task.id);
|
||||
setMsg(`「${task.name}」已开始执行`);
|
||||
} catch (e) {
|
||||
setMsg(`执行失败:${String(e)}`);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggle(task: ScheduledTask) {
|
||||
try {
|
||||
await api.setScheduledTaskEnabled(task.id, !task.enabled);
|
||||
await refreshScheduledTasks();
|
||||
} catch (e) {
|
||||
setMsg(`操作失败:${String(e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(task: ScheduledTask) {
|
||||
if (!confirm(`确定删除定时计划「${task.name}」吗?`)) return;
|
||||
try {
|
||||
await api.removeScheduledTask(task.id);
|
||||
setMsg("定时计划已删除");
|
||||
await refreshScheduledTasks();
|
||||
} catch (e) {
|
||||
setMsg(`删除失败:${String(e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto p-6">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">任务</h1>
|
||||
<p className="mt-1 text-sm text-slate-400">正在进行的下载与模型部署状态</p>
|
||||
<p className="mt-1 text-sm text-slate-400">
|
||||
定时计划由指定智能体到点自动执行;下方展示下载与模型部署进度
|
||||
</p>
|
||||
</div>
|
||||
{hasFinished ? (
|
||||
<button className="btn-secondary" onClick={clearFinishedTasks}>
|
||||
清除已完成
|
||||
<div className="flex shrink-0 gap-2">
|
||||
{hasFinished ? (
|
||||
<button className="btn-secondary" onClick={clearFinishedTasks}>
|
||||
清除已完成
|
||||
</button>
|
||||
) : null}
|
||||
<button className="btn-primary" onClick={openNew} disabled={busy}>
|
||||
+ 新建定时计划
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{msg ? (
|
||||
<div className="mb-4 flex items-center justify-between rounded-lg border border-border bg-panel px-4 py-2 text-xs text-slate-300">
|
||||
<span>{msg}</span>
|
||||
<button className="text-slate-500 hover:text-slate-200" onClick={() => setMsg(null)}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<section className="mb-8">
|
||||
<div className="mb-2 flex items-center gap-2 text-xs uppercase text-slate-500">
|
||||
定时计划
|
||||
<span className="rounded bg-panel-2 px-1.5 py-0.5 text-[10px] text-slate-400">
|
||||
{scheduledTasks.length}
|
||||
</span>
|
||||
</div>
|
||||
{scheduledTasks.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-border bg-panel/50 px-4 py-8 text-center text-sm text-slate-500">
|
||||
还没有定时计划,点击右上角「新建定时计划」,选择智能体(默认通用助手)与执行内容,到点自动运行
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-3 xl:grid-cols-2">
|
||||
{scheduledTasks.map((task) => (
|
||||
<ScheduledTaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
agentNameById={agentNameById}
|
||||
onRun={() => handleRunNow(task)}
|
||||
onEdit={() => openEdit(task)}
|
||||
onToggle={() => handleToggle(task)}
|
||||
onDelete={() => handleDelete(task)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className="mb-2 text-xs uppercase text-slate-500">下载 / 部署任务</div>
|
||||
{sorted.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-border bg-panel/50 px-4 py-8 text-center text-sm text-slate-500">
|
||||
暂无任务,去模型广场下载模型或部署本地模型后会自动出现在这里
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{sorted.map((t) => (
|
||||
<TaskRow key={t.id} task={t} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{showModal ? (
|
||||
<ScheduleModal
|
||||
task={editing}
|
||||
agents={agents}
|
||||
defaultAgentId={defaultAgentId}
|
||||
busy={busy}
|
||||
onSave={handleSave}
|
||||
onClose={() => setShowModal(false)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ScheduledTaskCard({
|
||||
task,
|
||||
agentNameById,
|
||||
onRun,
|
||||
onEdit,
|
||||
onToggle,
|
||||
onDelete,
|
||||
}: {
|
||||
task: ScheduledTask;
|
||||
agentNameById: Map<string, { name: string; icon: string }>;
|
||||
onRun: () => void;
|
||||
onEdit: () => void;
|
||||
onToggle: () => void;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const agent = task.agent_id ? agentNameById.get(task.agent_id) : undefined;
|
||||
const agentName = agent?.name ?? (task.agent_id ? "通用助手" : "普通对话");
|
||||
const agentIcon = agent?.icon ?? (task.agent_id ? "🤖" : "🗒️");
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col rounded-xl border border-border bg-panel p-4 ${
|
||||
task.enabled ? "" : "opacity-60"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-panel-2 text-xl">
|
||||
<Icon name="clock" className="h-5 w-5 text-slate-400" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate text-sm font-semibold">{task.name}</span>
|
||||
<ScheduleStatusChip status={task.last_status} enabled={task.enabled} />
|
||||
</div>
|
||||
<div className="mt-0.5 flex items-center gap-1 text-xs text-slate-400">
|
||||
<span>{agentIcon}</span>
|
||||
<span className="truncate">
|
||||
执行智能体:{agentName}
|
||||
{!task.enabled ? " · 已停用" : ""}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Toggle checked={task.enabled} onChange={onToggle} />
|
||||
</div>
|
||||
|
||||
<div className="mt-2 line-clamp-2 text-xs text-slate-400">{task.prompt}</div>
|
||||
|
||||
<div className="mt-3 space-y-1 text-[11px] text-slate-500">
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon name="calendar" className="h-3 w-3" />
|
||||
<span>
|
||||
{fmtInterval(task.interval_minutes)}
|
||||
{task.enabled ? ` · 下次执行 ${fmtDbTime(task.next_run_at)}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
{task.last_run_at ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon name="refresh" className="h-3 w-3" />
|
||||
<span>上次执行 {fmtDbTime(task.last_run_at)}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{task.last_error ? (
|
||||
<div className="line-clamp-2 text-red-400/90">失败原因:{task.last_error}</div>
|
||||
) : task.last_result ? (
|
||||
<div className="line-clamp-2 text-emerald-400/80">结果:{task.last_result}</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{sorted.length === 0 ? (
|
||||
<div className="mt-16 text-center text-sm text-slate-500">
|
||||
暂无任务,去模型广场下载模型或部署本地模型后会自动出现在这里
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{sorted.map((t) => (
|
||||
<TaskRow key={t.id} task={t} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<button className="btn-primary flex-1 px-2 py-1.5 text-xs" onClick={onRun}>
|
||||
<Icon name="play" className="mr-1 inline h-3.5 w-3.5" />
|
||||
立即执行
|
||||
</button>
|
||||
<button
|
||||
className="rounded-lg border border-border bg-panel-2 px-2.5 py-1.5 text-xs text-slate-400 hover:text-slate-200"
|
||||
onClick={onEdit}
|
||||
title="编辑"
|
||||
>
|
||||
<Icon name="edit" className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
className="rounded-lg border border-border bg-panel-2 px-2.5 py-1.5 text-xs text-slate-400 hover:text-red-400"
|
||||
onClick={onDelete}
|
||||
title="删除"
|
||||
>
|
||||
<Icon name="trash" className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ScheduleStatusChip({ status, enabled }: { status: string; enabled: boolean }) {
|
||||
if (!enabled) {
|
||||
return (
|
||||
<span className="shrink-0 rounded bg-panel-2 px-2 py-0.5 text-xs text-slate-400">
|
||||
已停用
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const map: Record<string, { cls: string; label: string }> = {
|
||||
idle: { cls: "bg-slate-500/20 text-slate-300", label: "待执行" },
|
||||
running: { cls: "bg-sky-500/20 text-sky-300", label: "执行中" },
|
||||
success: { cls: "bg-emerald-500/20 text-emerald-300", label: "成功" },
|
||||
error: { cls: "bg-red-500/20 text-red-300", label: "失败" },
|
||||
};
|
||||
const s = map[status] ?? map.idle;
|
||||
return <span className={`shrink-0 rounded px-2 py-0.5 text-xs ${s.cls}`}>{s.label}</span>;
|
||||
}
|
||||
|
||||
function ScheduleModal({
|
||||
task,
|
||||
agents,
|
||||
defaultAgentId,
|
||||
busy,
|
||||
onSave,
|
||||
onClose,
|
||||
}: {
|
||||
task: ScheduledTask | null;
|
||||
agents: { id: string; name: string; icon: string; enabled: boolean }[];
|
||||
defaultAgentId: string;
|
||||
busy: boolean;
|
||||
onSave: (draft: Draft) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [draft, setDraft] = useState<Draft>(() => ({
|
||||
name: task?.name ?? "",
|
||||
agentId: task?.agent_id ?? defaultAgentId,
|
||||
prompt: task?.prompt ?? "",
|
||||
intervalValue: task ? splitInterval(task.interval_minutes).value : 1,
|
||||
intervalUnit: task ? splitInterval(task.interval_minutes).unit : "hour",
|
||||
enabled: task?.enabled ?? true,
|
||||
}));
|
||||
|
||||
const valid = draft.name.trim() && draft.prompt.trim() && draft.intervalValue >= 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-6"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="flex max-h-[90vh] w-full max-w-lg flex-col overflow-hidden rounded-xl border border-border bg-panel"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-border px-5 py-3">
|
||||
<span className="text-sm font-medium">{task ? "编辑定时计划" : "新建定时计划"}</span>
|
||||
<button className="text-slate-500 hover:text-slate-200" onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-4 overflow-auto px-5 py-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-slate-400">计划名称</label>
|
||||
<input
|
||||
className="input"
|
||||
value={draft.name}
|
||||
placeholder="例如:每日工作总结"
|
||||
onChange={(e) => setDraft({ ...draft, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-slate-400">执行智能体</label>
|
||||
<select
|
||||
className="input"
|
||||
value={draft.agentId}
|
||||
onChange={(e) => setDraft({ ...draft, agentId: e.target.value })}
|
||||
>
|
||||
<option value="">普通对话(不指定智能体)</option>
|
||||
{agents.map((a) => (
|
||||
<option key={a.id} value={a.id}>
|
||||
{a.icon} {a.name}
|
||||
{a.enabled ? "" : "(已停用)"}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="mt-1 text-[11px] text-slate-500">
|
||||
默认使用「通用助手」;执行时会使用该智能体的人设与默认模型
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-slate-400">执行内容</label>
|
||||
<textarea
|
||||
className="input min-h-[96px] resize-y"
|
||||
value={draft.prompt}
|
||||
placeholder="每次到点发送给智能体的任务内容,例如:把最近一周的会话按主题整理成报告"
|
||||
onChange={(e) => setDraft({ ...draft, prompt: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-slate-400">执行间隔</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{INTERVAL_PRESETS.map((p) => {
|
||||
const active =
|
||||
draft.intervalValue === p.value && draft.intervalUnit === p.unit;
|
||||
return (
|
||||
<button
|
||||
key={p.label}
|
||||
type="button"
|
||||
className={`rounded-lg border px-3 py-1.5 text-xs transition-colors ${
|
||||
active
|
||||
? "border-accent bg-accent/20 text-white"
|
||||
: "border-border bg-panel-2 text-slate-400 hover:text-slate-200"
|
||||
}`}
|
||||
onClick={() =>
|
||||
setDraft({ ...draft, intervalValue: p.value, intervalUnit: p.unit })
|
||||
}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
className="input w-24"
|
||||
value={draft.intervalValue}
|
||||
onChange={(e) =>
|
||||
setDraft({ ...draft, intervalValue: Number(e.target.value) || 1 })
|
||||
}
|
||||
/>
|
||||
<select
|
||||
className="input w-28"
|
||||
value={draft.intervalUnit}
|
||||
onChange={(e) =>
|
||||
setDraft({ ...draft, intervalUnit: e.target.value as IntervalUnit })
|
||||
}
|
||||
>
|
||||
<option value="min">分钟</option>
|
||||
<option value="hour">小时</option>
|
||||
<option value="day">天</option>
|
||||
</select>
|
||||
<span className="text-xs text-slate-500">
|
||||
{fmtInterval(intervalToMinutes(draft.intervalValue, draft.intervalUnit))}执行一次
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Toggle
|
||||
checked={draft.enabled}
|
||||
onChange={(v) => setDraft({ ...draft, enabled: v })}
|
||||
/>
|
||||
<span className="text-xs text-slate-400">
|
||||
{draft.enabled ? "启用后到点自动执行" : "已停用,不会自动执行"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 border-t border-border px-5 py-3">
|
||||
<button className="btn-secondary" onClick={onClose} disabled={busy}>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={() => onSave(draft)}
|
||||
disabled={busy || !valid}
|
||||
>
|
||||
{busy ? "保存中…" : "保存"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function splitInterval(minutes: number): { value: number; unit: IntervalUnit } {
|
||||
if (minutes % 1440 === 0) return { value: minutes / 1440, unit: "day" };
|
||||
if (minutes % 60 === 0) return { value: minutes / 60, unit: "hour" };
|
||||
return { value: minutes, unit: "min" };
|
||||
}
|
||||
|
||||
function Toggle({
|
||||
checked,
|
||||
onChange,
|
||||
}: {
|
||||
checked: boolean;
|
||||
onChange: (v: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
onClick={() => onChange(!checked)}
|
||||
className={`relative h-6 w-11 shrink-0 rounded-full transition-colors ${
|
||||
checked ? "bg-accent" : "bg-panel-2"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 h-5 w-5 rounded-full bg-white shadow transition-all ${
|
||||
checked ? "left-[22px]" : "left-0.5"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskRow({ task }: { task: TaskItem }) {
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-panel p-4">
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Conversation,
|
||||
EngineStatus,
|
||||
ModelInfo,
|
||||
ScheduledTask,
|
||||
ServerStatus,
|
||||
Workflow,
|
||||
} from "./api";
|
||||
@@ -15,6 +16,7 @@ interface Store {
|
||||
agentConversations: Record<string, Conversation[]>;
|
||||
workflows: Workflow[];
|
||||
workflowsLoaded: boolean;
|
||||
scheduledTasks: ScheduledTask[];
|
||||
models: ModelInfo[];
|
||||
engine: EngineStatus | null;
|
||||
server: ServerStatus | null;
|
||||
@@ -30,6 +32,7 @@ interface Store {
|
||||
clearFinishedTasks: () => void;
|
||||
refreshAgents: () => Promise<void>;
|
||||
refreshWorkflows: () => Promise<void>;
|
||||
refreshScheduledTasks: () => Promise<void>;
|
||||
refreshModels: () => Promise<void>;
|
||||
refreshEngine: () => Promise<void>;
|
||||
refreshServer: () => Promise<void>;
|
||||
@@ -53,6 +56,7 @@ export const useStore = create<Store>((set) => ({
|
||||
agentConversations: {},
|
||||
workflows: [],
|
||||
workflowsLoaded: false,
|
||||
scheduledTasks: [],
|
||||
models: [],
|
||||
engine: null,
|
||||
server: null,
|
||||
@@ -88,6 +92,10 @@ export const useStore = create<Store>((set) => ({
|
||||
const workflows = await api.listWorkflows();
|
||||
set({ workflows, workflowsLoaded: true });
|
||||
},
|
||||
refreshScheduledTasks: async () => {
|
||||
const scheduledTasks = await api.listScheduledTasks();
|
||||
set({ scheduledTasks });
|
||||
},
|
||||
refreshModels: async () => set({ models: await api.listModels() }),
|
||||
refreshEngine: async () => set({ engine: await api.engineStatus() }),
|
||||
refreshServer: async () => set({ server: await api.serverStatus() }),
|
||||
|
||||
Reference in New Issue
Block a user