diff --git a/frontend-web/src/pages/OfficialDeerFlowEntry.tsx b/frontend-web/src/pages/OfficialDeerFlowEntry.tsx index df8f0f3e..cb815350 100644 --- a/frontend-web/src/pages/OfficialDeerFlowEntry.tsx +++ b/frontend-web/src/pages/OfficialDeerFlowEntry.tsx @@ -5,6 +5,7 @@ import { ChevronDown, CircleHelp, Clock3, + GitBranch, LoaderCircle, MessageSquareText, Network, @@ -42,8 +43,12 @@ import { OFFICIAL_DEERFLOW_HOME_ROUTE, OFFICIAL_DEERFLOW_ROUTE_PREFIX, } from "@/core/official-deerflow/routes"; +import { Toaster } from "@/components/ui/sonner"; import MultiAgentTasksPage from "./official-multi-agent-tasks/MultiAgentTasksPage"; +import TaskEditPage from "./official-multi-agent-tasks/TaskEditPage"; +import TaskAnalysisPage from "./official-task-analysis/TaskAnalysisPage"; +import TaskAnalysisDetailPage from "./official-task-analysis/TaskAnalysisDetailPage"; import "./official-deerflow-entry.css"; @@ -53,6 +58,9 @@ type OfficialPage = | "agent-chat" | "scheduled" | "multi-agent-tasks" + | "multi-agent-edit" + | "task-analysis" + | "task-analysis-detail" | "settings"; function pageFromPath(pathname: string): OfficialPage { @@ -62,7 +70,13 @@ function pageFromPath(pathname: string): OfficialPage { } if (path.startsWith("/workspace/agents")) return "agents"; if (path.startsWith("/workspace/scheduled-tasks")) return "scheduled"; + if (path.startsWith("/workspace/multi-agent-tasks/new")) return "multi-agent-edit"; + if (/^\/workspace\/multi-agent-tasks\/[^/]+\/edit$/.test(path)) { + return "multi-agent-edit"; + } if (path.startsWith("/workspace/multi-agent-tasks")) return "multi-agent-tasks"; + if (/^\/workspace\/task-analysis\/[^/]+/.test(path)) return "task-analysis-detail"; + if (path.startsWith("/workspace/task-analysis")) return "task-analysis"; if (path.startsWith("/workspace/settings")) return "settings"; return "chat"; } @@ -90,9 +104,31 @@ function parseScheduledTaskId(pathname: string) { function parseMultiAgentTaskId(pathname: string) { const match = pathname.match(/\/workspace\/multi-agent-tasks\/([^/]+)/); + const raw = match ? decodeURIComponent(match[1]!) : null; + // 编辑页路由(new / :id/edit)不是任务详情,不驱动列表页的抽屉。 + if (!raw || raw === "new") return null; + return raw; +} + +function parseTaskAnalysisId(pathname: string) { + const match = pathname.match(/\/workspace\/task-analysis\/([^/]+)/); return match ? decodeURIComponent(match[1]!) : null; } +function parseMultiAgentEditTarget(pathname: string): { + mode: "create" | "edit"; + taskId: string | null; +} { + const path = pathname.slice(OFFICIAL_DEERFLOW_ROUTE_PREFIX.length); + if (path.startsWith("/workspace/multi-agent-tasks/new")) { + return { mode: "create", taskId: null }; + } + const match = path.match(/^\/workspace\/multi-agent-tasks\/([^/]+)\/edit$/); + return match + ? { mode: "edit", taskId: decodeURIComponent(match[1]!) } + : { mode: "create", taskId: null }; +} + function readableError(error: unknown, fallback: string) { return error instanceof Error && error.message ? error.message : fallback; } @@ -677,6 +713,14 @@ export default function OfficialDeerFlowEntry() { () => parseMultiAgentTaskId(location.pathname), [location.pathname], ); + const multiAgentEditTarget = useMemo( + () => parseMultiAgentEditTarget(location.pathname), + [location.pathname], + ); + const taskAnalysisId = useMemo( + () => parseTaskAnalysisId(location.pathname), + [location.pathname], + ); const { data: threads = [], isLoading: threadsLoading, error: threadsError } = useThreads({ limit: 20, @@ -691,7 +735,8 @@ export default function OfficialDeerFlowEntry() { { icon: MessageSquareText, label: "对话", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/chats/new`, pages: ["chat"] }, { icon: Bot, label: "智能体", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/agents`, pages: ["agents", "agent-chat"] }, { icon: CalendarClock, label: "定时任务", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/scheduled-tasks`, pages: ["scheduled"] }, - { icon: Network, label: "多智能体定时任务", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/multi-agent-tasks`, pages: ["multi-agent-tasks"] }, + { icon: Network, label: "多智能体定时任务", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/multi-agent-tasks`, pages: ["multi-agent-tasks", "multi-agent-edit"] }, + { icon: GitBranch, label: "任务分析定时", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/task-analysis`, pages: ["task-analysis", "task-analysis-detail"] }, ] as const; return ( @@ -792,8 +837,20 @@ export default function OfficialDeerFlowEntry() { {page === "multi-agent-tasks" && ( )} + {page === "multi-agent-edit" && ( + + )} + {page === "task-analysis" && } + {page === "task-analysis-detail" && ( + + )} {page === "settings" && } + ); } diff --git a/frontend-web/src/pages/official-multi-agent-tasks/MultiAgentTasksPage.tsx b/frontend-web/src/pages/official-multi-agent-tasks/MultiAgentTasksPage.tsx index ba7055ab..3671278a 100644 --- a/frontend-web/src/pages/official-multi-agent-tasks/MultiAgentTasksPage.tsx +++ b/frontend-web/src/pages/official-multi-agent-tasks/MultiAgentTasksPage.tsx @@ -1,7 +1,10 @@ import { CalendarClock, CircleAlert, + CircleCheck, Clock3, + History, + LoaderCircle, Network, Pencil, Pause, @@ -13,24 +16,14 @@ import { } from "lucide-react"; import { useCallback, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { - Button, - DialogPlugin, - Drawer, - Input, - MessagePlugin, - Popconfirm, - Switch, - Tabs, - Tag, -} from "tdesign-react"; +import { Button, DialogPlugin, Drawer, Input, Popconfirm, Switch, Tabs, Tag } from "tdesign-react"; +import { toast } from "sonner"; import { OFFICIAL_DEERFLOW_ROUTE_PREFIX } from "@/core/official-deerflow/routes"; import AgentFlow from "./components/AgentFlow"; -import TaskEditorDialog from "./components/TaskEditorDialog"; import { useMultiAgentTaskActions, useMultiAgentTasks } from "./store"; -import type { MatRunView, MatTask, MatTaskPayload, MatTaskView } from "./types"; +import type { MatRunView, MatTaskView } from "./types"; import { describeCron, expandStages, @@ -72,8 +65,6 @@ export default function MultiAgentTasksPage({ const actions = useMultiAgentTaskActions(); const [search, setSearch] = useState(""); const [filter, setFilter] = useState("all"); - const [editorTask, setEditorTask] = useState(null); - const [editorVisible, setEditorVisible] = useState(false); const [busyTaskId, setBusyTaskId] = useState(null); const [activeRunId, setActiveRunId] = useState(null); @@ -116,30 +107,13 @@ export default function MultiAgentTasksPage({ }, [tasks]); const openCreate = () => { - setEditorTask(null); - setEditorVisible(true); + navigate(`${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/multi-agent-tasks/new`); }; - const openEdit = (task: MatTask) => { - setEditorTask(task); - setEditorVisible(true); - }; - - const handleEditorSubmit = async (payload: MatTaskPayload) => { - try { - if (editorTask) { - await actions.update.mutateAsync({ id: editorTask.id, payload }); - void MessagePlugin.success("任务已更新"); - } else { - const created = await actions.create.mutateAsync(payload); - void MessagePlugin.success("任务已创建"); - if (created?.id) openTask(created.id); - } - setEditorVisible(false); - setEditorTask(null); - } catch { - void MessagePlugin.error("保存失败,请重试"); - } + const openEdit = (task: MatTaskView) => { + navigate( + `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/multi-agent-tasks/${encodeURIComponent(task.id)}/edit`, + ); }; const runTrigger = async (task: MatTaskView) => { @@ -148,9 +122,9 @@ export default function MultiAgentTasksPage({ await actions.trigger.mutateAsync(task.id); setActiveRunId(null); openTask(task.id); - void MessagePlugin.success("已触发执行"); + toast.success("已触发执行"); } catch { - void MessagePlugin.error("触发失败,请重试"); + toast.error("触发失败,请重试"); } finally { setBusyTaskId(null); } @@ -160,9 +134,9 @@ export default function MultiAgentTasksPage({ setBusyTaskId(task.id); try { await actions.setEnabled.mutateAsync({ id: task.id, enabled: !task.enabled }); - void MessagePlugin.success(task.enabled ? "任务已暂停" : "任务已恢复"); + toast.success(task.enabled ? "任务已暂停" : "任务已恢复"); } catch { - void MessagePlugin.error("操作失败,请重试"); + toast.error("操作失败,请重试"); } finally { setBusyTaskId(null); } @@ -178,7 +152,7 @@ export default function MultiAgentTasksPage({ dialog.destroy(); void actions.remove.mutateAsync(task.id).then(() => { if (selectedTask?.id === task.id) closeTask(); - void MessagePlugin.success("任务已删除"); + toast.success("任务已删除"); }); }, onClose: () => dialog.destroy(), @@ -192,43 +166,58 @@ export default function MultiAgentTasksPage({ return (
-
-
-

-

+
+

多智能体定时任务

-

+

把多个智能体编排成协作编队:同阶段并行、阶段间串行,按计划自动执行并沉淀运行记录。

-
-
-
- {stats.total} - 编排任务 -
-
- {stats.enabled} - 已启用 -
-
- {stats.running} - 执行中 -
-
- {stats.runTotal} - 累计执行 -
-
- -
+
+
+
+ +
+ {stats.total} + 编排任务 +
+
+
+ +
+ {stats.enabled} + 已启用 +
+
+
+ +
+ {stats.running} + 执行中 +
+
+
+ +
+ {stats.runTotal} + 累计执行 +
+
+
+
@@ -274,7 +263,6 @@ export default function MultiAgentTasksPage({

@@ -385,14 +373,9 @@ export default function MultiAgentTasksPage({ footer={ selectedTask ? (
- +
) : null } @@ -429,7 +417,7 @@ export default function MultiAgentTasksPage({

{selectedTask.name}

-

{describeCron(selectedTask.cron_expr)} · {selectedTask.timezone}

+

{describeCron(selectedTask.cron_expr)}

{(() => { const badge = taskBadgeOf(selectedTask); @@ -500,6 +488,7 @@ export default function MultiAgentTasksPage({ seatStates={activeRun?.seat_states ?? null} seats={selectedTask.seats} showGoals + size="sm" stageGoals={selectedTask.stage_goals} stages={selectedStages} /> @@ -590,17 +579,6 @@ export default function MultiAgentTasksPage({
)} - - { - setEditorVisible(false); - setEditorTask(null); - }} - onSubmit={handleEditorSubmit} - submitting={actions.create.isPending || actions.update.isPending} - task={editorTask} - visible={editorVisible} - />
); } diff --git a/frontend-web/src/pages/official-multi-agent-tasks/TaskEditPage.tsx b/frontend-web/src/pages/official-multi-agent-tasks/TaskEditPage.tsx new file mode 100644 index 00000000..1a5bdd97 --- /dev/null +++ b/frontend-web/src/pages/official-multi-agent-tasks/TaskEditPage.tsx @@ -0,0 +1,942 @@ +/** + * 多智能体定时任务 · 独立编辑页。 + * + * 布局:顶部是紧凑表单条(任务名称 + 任务描述),其余表单项(执行频率、任务指令、 + * 总控提示、启用开关)收进「执行设置」弹窗;主体是智能体编排。 + * 编排交互与业务链条编辑器一致(复制其 @dnd-kit 模式 + 复用 chain-stages 纯函数): + * - 左栏候选池:拖左侧手柄到右侧任意阶段 / 新建阶段落区;点卡片本体 = 追加为新阶段。 + * - 右栏阶段组:阶段是 droppable,席位是 sortable;席位可跨阶段拖动(变并行)或拖到 + * 「新建阶段」落区独立成阶段;按钮兜底(并入上/下一阶段、独立成阶段、移除)。 + * 保存时弹出确认弹窗:任务摘要 + 编排流程图(同阶段并行 · 阶段间串行)+ 图例。 + */ + +import { + ArrowLeft, + Bot, + ChevronDown, + ChevronUp, + Eye, + GripVertical, + Plus, + Save, + Search, + Settings2, + SplitSquareVertical, + Trash2, + X, +} from "lucide-react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { + DndContext, + DragOverlay, + PointerSensor, + closestCenter, + useDraggable, + useDroppable, + useSensor, + useSensors, + type DragEndEvent, + type DragStartEvent, +} from "@dnd-kit/core"; +import { + SortableContext, + useSortable, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { Button, Dialog, Input, Switch, Textarea } from "tdesign-react"; +import { toast } from "sonner"; + +import { useAgents } from "@/core/agents"; +import type { Agent } from "@/core/agents"; +import { + type ScheduleConfig, + buildCron, + parseCron, +} from "@/components/workspace/scheduled-tasks/schedule-builder"; +import { + addEmptyStage, + addSeatAsNewStage, + addSeatToStage, + countSeats, + deleteStage, + findStageIndexOfSeat, + moveSeatToStage, + moveStage, + removeSeat, + setStageGoal, + splitSeatToNewStage, + stageGroupsToStageGoals, + stageGroupsToStagePayload, + type StageGroups, +} from "@/roundtable-planning/lib/chain-stages"; + +import { OFFICIAL_DEERFLOW_ROUTE_PREFIX } from "@/core/official-deerflow/routes"; + +import AgentFlow from "./components/AgentFlow"; +import AgentDetailDrawer, { + type AgentDetailTarget, +} from "./components/AgentDetailDrawer"; +import SchedulePicker from "./components/SchedulePicker"; +import { useMultiAgentTaskActions, useMultiAgentTasks } from "./store"; +import type { MatSeat, MatTask, MatTaskPayload } from "./types"; +import { + defaultScheduleConfig, + describeCron, + describeScheduleConfig, + expandStages, +} from "./utils"; + +const POOL_PREFIX = "matpool::"; +const SEAT_PREFIX = "matseat::"; +const STAGE_PREFIX = "matstage::"; +const NEWSTAGE_DROPPABLE_ID = "mat-newstage"; + +const MAX_SEATS = 20; +const MIN_SEATS = 2; +const DEFAULT_TIMEZONE = "Asia/Shanghai"; + +type ActiveDrag = { name: string } | null; + +function seatFromAgent(agent: Agent): MatSeat { + return { + agent_id: agent.id, + name: agent.name, + description: agent.description ?? undefined, + }; +} + +function draftFromTask(task: MatTask) { + const stages = expandStages(task); + const seatById = new Map(task.seats.map((seat) => [seat.agent_id, seat])); + return { + name: task.name, + description: task.description ?? "", + schedule: parseCron(task.cron_expr) ?? defaultScheduleConfig(), + enabled: task.enabled, + prompt: task.prompt, + coordinatorPrompt: task.coordinator_prompt ?? "", + stageGroups: stages.map((stage, index) => ({ + seats: stage + .map((agentId) => seatById.get(agentId)) + .filter((seat): seat is MatSeat => !!seat), + goal: task.stage_goals?.[index] ?? "", + })) as StageGroups, + }; +} + +function groupsToPayloadSeats(groups: StageGroups): MatSeat[] { + return groups + .filter((group) => group.seats.length > 0) + .flatMap((group) => group.seats as MatSeat[]); +} + +// ── 左栏:候选智能体卡片(点卡片看详情 / 加号或手柄加入)────────────────── + +function PoolAgentCard({ + agent, + used, + onAdd, + onShowDetail, +}: { + agent: Agent; + used: boolean; + onAdd: () => void; + onShowDetail: () => void; +}) { + const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ + id: POOL_PREFIX + agent.id, + disabled: used, + }); + return ( +
+ + + {used ? ( + 已加入 + ) : ( + + )} +
+ ); +} + +// ── 右栏:单个席位长条(sortable)───────────────────────────────────────── + +function SeatStrip({ + seat, + globalIndex, + isFirstStage, + isLastStage, + onRemove, + onMergeUp, + onMergeDown, + onSplit, + onDetail, +}: { + seat: MatSeat; + globalIndex: number; + isFirstStage: boolean; + isLastStage: boolean; + onRemove: () => void; + onMergeUp: () => void; + onMergeDown: () => void; + onSplit: () => void; + onDetail: () => void; +}) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = + useSortable({ id: SEAT_PREFIX + seat.agent_id }); + return ( +
+ + {globalIndex + 1} + +
+ {seat.name} + {seat.description && {seat.description}} +
+ + + + + + + +
+ ); +} + +// ── 右栏:阶段容器(droppable + 组内 sortable)──────────────────────────── + +function StageBox({ + group, + stageIdx, + stageCount, + startIndex, + onGoalChange, + onStageReorder, + onStageDelete, + onRemoveSeat, + onMergeSeat, + onSplitSeat, + onSeatDetail, +}: { + group: StageGroups[number]; + stageIdx: number; + stageCount: number; + startIndex: number; + onGoalChange: (goal: string) => void; + onStageReorder: (dir: -1 | 1) => void; + onStageDelete: () => void; + onRemoveSeat: (agentId: string) => void; + onMergeSeat: (agentId: string, targetStage: number) => void; + onSplitSeat: (agentId: string) => void; + onSeatDetail: (seat: MatSeat) => void; +}) { + const { setNodeRef, isOver } = useDroppable({ id: STAGE_PREFIX + stageIdx }); + const seats = group.seats as MatSeat[]; + const isParallel = seats.length > 1; + const isFirstStage = stageIdx === 0; + const isLastStage = stageIdx === stageCount - 1; + const seatItemIds = seats.map((seat) => SEAT_PREFIX + seat.agent_id); + + return ( +
+
+ 阶段 {stageIdx + 1} + {isParallel ? ( + 并行 ×{seats.length} + ) : ( + 串行 + )} + + + + + +
+