代码合并
This commit is contained in:
1 parent
673adef7df
commit
30e1c2dd90
21 files changed
+6033
-1095
No files matched your search
@@ -5,6 +5,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
Clock3,
|
Clock3,
|
||||||
|
GitBranch,
|
||||||
LoaderCircle,
|
LoaderCircle,
|
||||||
MessageSquareText,
|
MessageSquareText,
|
||||||
Network,
|
Network,
|
||||||
@@ -42,8 +43,12 @@ import {
|
|||||||
OFFICIAL_DEERFLOW_HOME_ROUTE,
|
OFFICIAL_DEERFLOW_HOME_ROUTE,
|
||||||
OFFICIAL_DEERFLOW_ROUTE_PREFIX,
|
OFFICIAL_DEERFLOW_ROUTE_PREFIX,
|
||||||
} from "@/core/official-deerflow/routes";
|
} from "@/core/official-deerflow/routes";
|
||||||
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
|
||||||
import MultiAgentTasksPage from "./official-multi-agent-tasks/MultiAgentTasksPage";
|
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";
|
import "./official-deerflow-entry.css";
|
||||||
|
|
||||||
@@ -53,6 +58,9 @@ type OfficialPage =
|
|||||||
| "agent-chat"
|
| "agent-chat"
|
||||||
| "scheduled"
|
| "scheduled"
|
||||||
| "multi-agent-tasks"
|
| "multi-agent-tasks"
|
||||||
|
| "multi-agent-edit"
|
||||||
|
| "task-analysis"
|
||||||
|
| "task-analysis-detail"
|
||||||
| "settings";
|
| "settings";
|
||||||
|
|
||||||
function pageFromPath(pathname: string): OfficialPage {
|
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/agents")) return "agents";
|
||||||
if (path.startsWith("/workspace/scheduled-tasks")) return "scheduled";
|
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 (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";
|
if (path.startsWith("/workspace/settings")) return "settings";
|
||||||
return "chat";
|
return "chat";
|
||||||
}
|
}
|
||||||
@@ -90,9 +104,31 @@ function parseScheduledTaskId(pathname: string) {
|
|||||||
|
|
||||||
function parseMultiAgentTaskId(pathname: string) {
|
function parseMultiAgentTaskId(pathname: string) {
|
||||||
const match = pathname.match(/\/workspace\/multi-agent-tasks\/([^/]+)/);
|
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;
|
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) {
|
function readableError(error: unknown, fallback: string) {
|
||||||
return error instanceof Error && error.message ? error.message : fallback;
|
return error instanceof Error && error.message ? error.message : fallback;
|
||||||
}
|
}
|
||||||
@@ -677,6 +713,14 @@ export default function OfficialDeerFlowEntry() {
|
|||||||
() => parseMultiAgentTaskId(location.pathname),
|
() => parseMultiAgentTaskId(location.pathname),
|
||||||
[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 } =
|
const { data: threads = [], isLoading: threadsLoading, error: threadsError } =
|
||||||
useThreads({
|
useThreads({
|
||||||
limit: 20,
|
limit: 20,
|
||||||
@@ -691,7 +735,8 @@ export default function OfficialDeerFlowEntry() {
|
|||||||
{ icon: MessageSquareText, label: "对话", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/chats/new`, pages: ["chat"] },
|
{ 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: 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: 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;
|
] as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -792,8 +837,20 @@ export default function OfficialDeerFlowEntry() {
|
|||||||
{page === "multi-agent-tasks" && (
|
{page === "multi-agent-tasks" && (
|
||||||
<MultiAgentTasksPage routeTaskId={multiAgentTaskId} />
|
<MultiAgentTasksPage routeTaskId={multiAgentTaskId} />
|
||||||
)}
|
)}
|
||||||
|
{page === "multi-agent-edit" && (
|
||||||
|
<TaskEditPage
|
||||||
|
key={multiAgentEditTarget.taskId ?? "create"}
|
||||||
|
mode={multiAgentEditTarget.mode}
|
||||||
|
taskId={multiAgentEditTarget.taskId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{page === "task-analysis" && <TaskAnalysisPage />}
|
||||||
|
{page === "task-analysis-detail" && (
|
||||||
|
<TaskAnalysisDetailPage routeTaskId={taskAnalysisId} />
|
||||||
|
)}
|
||||||
{page === "settings" && <SettingsPage />}
|
{page === "settings" && <SettingsPage />}
|
||||||
</div>
|
</div>
|
||||||
|
<Toaster position="top-center" />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
CalendarClock,
|
CalendarClock,
|
||||||
CircleAlert,
|
CircleAlert,
|
||||||
|
CircleCheck,
|
||||||
Clock3,
|
Clock3,
|
||||||
|
History,
|
||||||
|
LoaderCircle,
|
||||||
Network,
|
Network,
|
||||||
Pencil,
|
Pencil,
|
||||||
Pause,
|
Pause,
|
||||||
@@ -13,24 +16,14 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import {
|
import { Button, DialogPlugin, Drawer, Input, Popconfirm, Switch, Tabs, Tag } from "tdesign-react";
|
||||||
Button,
|
import { toast } from "sonner";
|
||||||
DialogPlugin,
|
|
||||||
Drawer,
|
|
||||||
Input,
|
|
||||||
MessagePlugin,
|
|
||||||
Popconfirm,
|
|
||||||
Switch,
|
|
||||||
Tabs,
|
|
||||||
Tag,
|
|
||||||
} from "tdesign-react";
|
|
||||||
|
|
||||||
import { OFFICIAL_DEERFLOW_ROUTE_PREFIX } from "@/core/official-deerflow/routes";
|
import { OFFICIAL_DEERFLOW_ROUTE_PREFIX } from "@/core/official-deerflow/routes";
|
||||||
|
|
||||||
import AgentFlow from "./components/AgentFlow";
|
import AgentFlow from "./components/AgentFlow";
|
||||||
import TaskEditorDialog from "./components/TaskEditorDialog";
|
|
||||||
import { useMultiAgentTaskActions, useMultiAgentTasks } from "./store";
|
import { useMultiAgentTaskActions, useMultiAgentTasks } from "./store";
|
||||||
import type { MatRunView, MatTask, MatTaskPayload, MatTaskView } from "./types";
|
import type { MatRunView, MatTaskView } from "./types";
|
||||||
import {
|
import {
|
||||||
describeCron,
|
describeCron,
|
||||||
expandStages,
|
expandStages,
|
||||||
@@ -72,8 +65,6 @@ export default function MultiAgentTasksPage({
|
|||||||
const actions = useMultiAgentTaskActions();
|
const actions = useMultiAgentTaskActions();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [filter, setFilter] = useState<StatusFilter>("all");
|
const [filter, setFilter] = useState<StatusFilter>("all");
|
||||||
const [editorTask, setEditorTask] = useState<MatTask | null>(null);
|
|
||||||
const [editorVisible, setEditorVisible] = useState(false);
|
|
||||||
const [busyTaskId, setBusyTaskId] = useState<string | null>(null);
|
const [busyTaskId, setBusyTaskId] = useState<string | null>(null);
|
||||||
const [activeRunId, setActiveRunId] = useState<string | null>(null);
|
const [activeRunId, setActiveRunId] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -116,30 +107,13 @@ export default function MultiAgentTasksPage({
|
|||||||
}, [tasks]);
|
}, [tasks]);
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditorTask(null);
|
navigate(`${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/multi-agent-tasks/new`);
|
||||||
setEditorVisible(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEdit = (task: MatTask) => {
|
const openEdit = (task: MatTaskView) => {
|
||||||
setEditorTask(task);
|
navigate(
|
||||||
setEditorVisible(true);
|
`${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/multi-agent-tasks/${encodeURIComponent(task.id)}/edit`,
|
||||||
};
|
);
|
||||||
|
|
||||||
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 runTrigger = async (task: MatTaskView) => {
|
const runTrigger = async (task: MatTaskView) => {
|
||||||
@@ -148,9 +122,9 @@ export default function MultiAgentTasksPage({
|
|||||||
await actions.trigger.mutateAsync(task.id);
|
await actions.trigger.mutateAsync(task.id);
|
||||||
setActiveRunId(null);
|
setActiveRunId(null);
|
||||||
openTask(task.id);
|
openTask(task.id);
|
||||||
void MessagePlugin.success("已触发执行");
|
toast.success("已触发执行");
|
||||||
} catch {
|
} catch {
|
||||||
void MessagePlugin.error("触发失败,请重试");
|
toast.error("触发失败,请重试");
|
||||||
} finally {
|
} finally {
|
||||||
setBusyTaskId(null);
|
setBusyTaskId(null);
|
||||||
}
|
}
|
||||||
@@ -160,9 +134,9 @@ export default function MultiAgentTasksPage({
|
|||||||
setBusyTaskId(task.id);
|
setBusyTaskId(task.id);
|
||||||
try {
|
try {
|
||||||
await actions.setEnabled.mutateAsync({ id: task.id, enabled: !task.enabled });
|
await actions.setEnabled.mutateAsync({ id: task.id, enabled: !task.enabled });
|
||||||
void MessagePlugin.success(task.enabled ? "任务已暂停" : "任务已恢复");
|
toast.success(task.enabled ? "任务已暂停" : "任务已恢复");
|
||||||
} catch {
|
} catch {
|
||||||
void MessagePlugin.error("操作失败,请重试");
|
toast.error("操作失败,请重试");
|
||||||
} finally {
|
} finally {
|
||||||
setBusyTaskId(null);
|
setBusyTaskId(null);
|
||||||
}
|
}
|
||||||
@@ -178,7 +152,7 @@ export default function MultiAgentTasksPage({
|
|||||||
dialog.destroy();
|
dialog.destroy();
|
||||||
void actions.remove.mutateAsync(task.id).then(() => {
|
void actions.remove.mutateAsync(task.id).then(() => {
|
||||||
if (selectedTask?.id === task.id) closeTask();
|
if (selectedTask?.id === task.id) closeTask();
|
||||||
void MessagePlugin.success("任务已删除");
|
toast.success("任务已删除");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onClose: () => dialog.destroy(),
|
onClose: () => dialog.destroy(),
|
||||||
@@ -192,42 +166,57 @@ export default function MultiAgentTasksPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mat-page">
|
<section className="mat-page">
|
||||||
<header className="mat-hero">
|
<header className="mat-page-head">
|
||||||
<div className="mat-hero-copy">
|
<div className="mat-page-head-copy">
|
||||||
<p className="mat-hero-eyebrow">
|
|
||||||
<Network aria-hidden="true" size={14} />
|
|
||||||
编排 · 协作 · 自动执行
|
|
||||||
</p>
|
|
||||||
<h1>多智能体定时任务</h1>
|
<h1>多智能体定时任务</h1>
|
||||||
<p className="mat-hero-desc">
|
<p>
|
||||||
把多个智能体编排成协作编队:同阶段并行、阶段间串行,按计划自动执行并沉淀运行记录。
|
把多个智能体编排成协作编队:同阶段并行、阶段间串行,按计划自动执行并沉淀运行记录。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mat-hero-side">
|
<Button theme="primary" onClick={openCreate}>
|
||||||
<div className="mat-hero-stats">
|
<Plus aria-hidden="true" size={15} />
|
||||||
<div className="mat-hero-stat">
|
新建编排任务
|
||||||
|
</Button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="mat-stats" aria-label="任务统计">
|
||||||
|
<div className="mat-stat">
|
||||||
|
<span className="mat-stat-icon is-primary" aria-hidden="true">
|
||||||
|
<Network size={18} />
|
||||||
|
</span>
|
||||||
|
<div className="mat-stat-copy">
|
||||||
<strong>{stats.total}</strong>
|
<strong>{stats.total}</strong>
|
||||||
<span>编排任务</span>
|
<span>编排任务</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mat-hero-stat">
|
</div>
|
||||||
|
<div className="mat-stat">
|
||||||
|
<span className="mat-stat-icon is-success" aria-hidden="true">
|
||||||
|
<CircleCheck size={18} />
|
||||||
|
</span>
|
||||||
|
<div className="mat-stat-copy">
|
||||||
<strong>{stats.enabled}</strong>
|
<strong>{stats.enabled}</strong>
|
||||||
<span>已启用</span>
|
<span>已启用</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mat-hero-stat">
|
</div>
|
||||||
<strong>{stats.running}</strong>
|
<div className="mat-stat">
|
||||||
|
<span className={`mat-stat-icon${stats.running ? " is-live" : " is-muted"}`} aria-hidden="true">
|
||||||
|
{stats.running ? <LoaderCircle size={18} className="mat-spin" /> : <Clock3 size={18} />}
|
||||||
|
</span>
|
||||||
|
<div className="mat-stat-copy">
|
||||||
|
<strong className={stats.running ? "is-live" : undefined}>{stats.running}</strong>
|
||||||
<span>执行中</span>
|
<span>执行中</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mat-hero-stat">
|
</div>
|
||||||
|
<div className="mat-stat">
|
||||||
|
<span className="mat-stat-icon is-info" aria-hidden="true">
|
||||||
|
<History size={18} />
|
||||||
|
</span>
|
||||||
|
<div className="mat-stat-copy">
|
||||||
<strong>{stats.runTotal}</strong>
|
<strong>{stats.runTotal}</strong>
|
||||||
<span>累计执行</span>
|
<span>累计执行</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button className="mat-hero-create" onClick={openCreate} theme="default">
|
|
||||||
<Plus aria-hidden="true" size={16} />
|
|
||||||
新建编排任务
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="mat-body">
|
<div className="mat-body">
|
||||||
<div className="mat-toolbar">
|
<div className="mat-toolbar">
|
||||||
@@ -274,7 +263,6 @@ export default function MultiAgentTasksPage({
|
|||||||
<p>
|
<p>
|
||||||
<CalendarClock aria-hidden="true" size={13} />
|
<CalendarClock aria-hidden="true" size={13} />
|
||||||
{describeCron(task.cron_expr)}
|
{describeCron(task.cron_expr)}
|
||||||
<em> · {task.timezone}</em>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Tag size="small" theme={badge.theme} variant="light">
|
<Tag size="small" theme={badge.theme} variant="light">
|
||||||
@@ -385,14 +373,9 @@ export default function MultiAgentTasksPage({
|
|||||||
footer={
|
footer={
|
||||||
selectedTask ? (
|
selectedTask ? (
|
||||||
<div className="mat-detail-footer">
|
<div className="mat-detail-footer">
|
||||||
<Button
|
<Button variant="outline" onClick={() => openEdit(selectedTask)}>
|
||||||
disabled={busyTaskId === selectedTask.id}
|
<Pencil aria-hidden="true" size={15} />
|
||||||
loading={busyTaskId === selectedTask.id}
|
编辑编排
|
||||||
theme="primary"
|
|
||||||
onClick={() => void runTrigger(selectedTask)}
|
|
||||||
>
|
|
||||||
<Zap aria-hidden="true" size={15} />
|
|
||||||
立即执行
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -405,10 +388,6 @@ export default function MultiAgentTasksPage({
|
|||||||
)}
|
)}
|
||||||
{selectedTask.enabled ? "暂停任务" : "恢复任务"}
|
{selectedTask.enabled ? "暂停任务" : "恢复任务"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" onClick={() => openEdit(selectedTask)}>
|
|
||||||
<Pencil aria-hidden="true" size={15} />
|
|
||||||
编辑编排
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
className="mat-danger-btn"
|
className="mat-danger-btn"
|
||||||
theme="danger"
|
theme="danger"
|
||||||
@@ -418,6 +397,15 @@ export default function MultiAgentTasksPage({
|
|||||||
<Trash2 aria-hidden="true" size={15} />
|
<Trash2 aria-hidden="true" size={15} />
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={busyTaskId === selectedTask.id}
|
||||||
|
loading={busyTaskId === selectedTask.id}
|
||||||
|
theme="primary"
|
||||||
|
onClick={() => void runTrigger(selectedTask)}
|
||||||
|
>
|
||||||
|
<Zap aria-hidden="true" size={15} />
|
||||||
|
立即执行
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
@@ -429,7 +417,7 @@ export default function MultiAgentTasksPage({
|
|||||||
</span>
|
</span>
|
||||||
<div className="mat-detail-heading">
|
<div className="mat-detail-heading">
|
||||||
<h3>{selectedTask.name}</h3>
|
<h3>{selectedTask.name}</h3>
|
||||||
<p>{describeCron(selectedTask.cron_expr)} · {selectedTask.timezone}</p>
|
<p>{describeCron(selectedTask.cron_expr)}</p>
|
||||||
</div>
|
</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const badge = taskBadgeOf(selectedTask);
|
const badge = taskBadgeOf(selectedTask);
|
||||||
@@ -500,6 +488,7 @@ export default function MultiAgentTasksPage({
|
|||||||
seatStates={activeRun?.seat_states ?? null}
|
seatStates={activeRun?.seat_states ?? null}
|
||||||
seats={selectedTask.seats}
|
seats={selectedTask.seats}
|
||||||
showGoals
|
showGoals
|
||||||
|
size="sm"
|
||||||
stageGoals={selectedTask.stage_goals}
|
stageGoals={selectedTask.stage_goals}
|
||||||
stages={selectedStages}
|
stages={selectedStages}
|
||||||
/>
|
/>
|
||||||
@@ -590,17 +579,6 @@ export default function MultiAgentTasksPage({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
<TaskEditorDialog
|
|
||||||
onClose={() => {
|
|
||||||
setEditorVisible(false);
|
|
||||||
setEditorTask(null);
|
|
||||||
}}
|
|
||||||
onSubmit={handleEditorSubmit}
|
|
||||||
submitting={actions.create.isPending || actions.update.isPending}
|
|
||||||
task={editorTask}
|
|
||||||
visible={editorVisible}
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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 (
|
||||||
|
<div
|
||||||
|
className={`mat-pool-item${used ? " is-used" : ""}${isDragging ? " is-dragging" : ""}`}
|
||||||
|
ref={setNodeRef}
|
||||||
|
{...attributes}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label={used ? "已在编排中" : "拖动加入编排"}
|
||||||
|
className="mat-pool-grip"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
title={used ? "已在编排中" : "拖动到右侧阶段加入(可与其它智能体并行)"}
|
||||||
|
type="button"
|
||||||
|
{...(used ? {} : listeners)}
|
||||||
|
>
|
||||||
|
<GripVertical size={14} />
|
||||||
|
</button>
|
||||||
|
<button className="mat-pool-main" onClick={onShowDetail} type="button">
|
||||||
|
<span className="mat-pool-icon" aria-hidden="true">
|
||||||
|
<Bot size={13} />
|
||||||
|
</span>
|
||||||
|
<span className="mat-pool-copy">
|
||||||
|
<span className="mat-pool-item-name">{agent.name}</span>
|
||||||
|
<span className="mat-pool-item-desc">{agent.description || "暂无简介"}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{used ? (
|
||||||
|
<span className="mat-pool-used">已加入</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
aria-label={`加入 ${agent.name}`}
|
||||||
|
className="mat-pool-join"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onAdd();
|
||||||
|
}}
|
||||||
|
title="追加为新阶段"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Plus size={13} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 右栏:单个席位长条(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 (
|
||||||
|
<div
|
||||||
|
className={`mat-seat-row${isDragging ? " is-dragging" : ""}`}
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={{ transform: CSS.Transform.toString(transform), transition }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="拖动调整编排"
|
||||||
|
className="mat-seat-row-grip"
|
||||||
|
title="拖动到其它阶段(并行)/ 排序 / 拖到底部虚线框独立成阶段"
|
||||||
|
type="button"
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
<GripVertical size={14} />
|
||||||
|
</button>
|
||||||
|
<span className="mat-seat-row-no">{globalIndex + 1}</span>
|
||||||
|
<span className="mat-seat-row-icon" aria-hidden="true">
|
||||||
|
<Bot size={13} />
|
||||||
|
</span>
|
||||||
|
<div className="mat-seat-row-copy" title={seat.description}>
|
||||||
|
<span className="mat-seat-row-name">{seat.name}</span>
|
||||||
|
{seat.description && <span className="mat-seat-row-desc">{seat.description}</span>}
|
||||||
|
</div>
|
||||||
|
<span className="mat-seat-row-ops">
|
||||||
|
<button
|
||||||
|
aria-label={`查看 ${seat.name} 详情`}
|
||||||
|
onClick={onDetail}
|
||||||
|
title="查看详情"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Eye size={14} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label="并入上一阶段(与其并行)"
|
||||||
|
disabled={isFirstStage}
|
||||||
|
onClick={onMergeUp}
|
||||||
|
title="并入上一阶段(与其并行)"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<ChevronUp size={14} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label="独立成阶段"
|
||||||
|
onClick={onSplit}
|
||||||
|
title="独立成阶段"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<SplitSquareVertical size={14} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label="并入下一阶段(与其并行)"
|
||||||
|
disabled={isLastStage}
|
||||||
|
onClick={onMergeDown}
|
||||||
|
title="并入下一阶段(与其并行)"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<ChevronDown size={14} />
|
||||||
|
</button>
|
||||||
|
<button aria-label="移除智能体" onClick={onRemove} title="移除" type="button">
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 右栏:阶段容器(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 (
|
||||||
|
<article className={`mat-stage-box${isOver ? " is-over" : ""}`}>
|
||||||
|
<header className="mat-stage-box-head">
|
||||||
|
<span className="mat-stage-box-no">阶段 {stageIdx + 1}</span>
|
||||||
|
{isParallel ? (
|
||||||
|
<span className="mat-flow-mode is-parallel">并行 ×{seats.length}</span>
|
||||||
|
) : (
|
||||||
|
<span className="mat-flow-mode is-serial">串行</span>
|
||||||
|
)}
|
||||||
|
<span className="mat-stage-box-ops">
|
||||||
|
<button
|
||||||
|
aria-label="阶段上移"
|
||||||
|
disabled={isFirstStage}
|
||||||
|
onClick={() => onStageReorder(-1)}
|
||||||
|
title="上移"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<ChevronUp size={14} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label="阶段下移"
|
||||||
|
disabled={isLastStage}
|
||||||
|
onClick={() => onStageReorder(1)}
|
||||||
|
title="下移"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<ChevronDown size={14} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label="删除阶段"
|
||||||
|
onClick={onStageDelete}
|
||||||
|
title="删除整个阶段"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<Textarea
|
||||||
|
autosize
|
||||||
|
className="mat-stage-goal"
|
||||||
|
maxlength={120}
|
||||||
|
placeholder="本阶段目标(可选):本阶段要产出什么、如何衔接上一阶段"
|
||||||
|
value={group.goal ?? ""}
|
||||||
|
onChange={(value) => onGoalChange(String(value ?? ""))}
|
||||||
|
/>
|
||||||
|
<div className="mat-stage-seats" ref={setNodeRef}>
|
||||||
|
{seats.length === 0 ? (
|
||||||
|
<div className="mat-stage-seat-empty">空阶段 —— 把左侧智能体拖到此处,或删除该阶段。</div>
|
||||||
|
) : (
|
||||||
|
<SortableContext items={seatItemIds} strategy={verticalListSortingStrategy}>
|
||||||
|
{seats.map((seat, index) => (
|
||||||
|
<SeatStrip
|
||||||
|
globalIndex={startIndex + index}
|
||||||
|
isLastStage={isLastStage}
|
||||||
|
isFirstStage={isFirstStage}
|
||||||
|
key={seat.agent_id}
|
||||||
|
onDetail={() => onSeatDetail(seat)}
|
||||||
|
onMergeDown={() => onMergeSeat(seat.agent_id, stageIdx + 1)}
|
||||||
|
onMergeUp={() => onMergeSeat(seat.agent_id, stageIdx - 1)}
|
||||||
|
onRemove={() => onRemoveSeat(seat.agent_id)}
|
||||||
|
onSplit={() => onSplitSeat(seat.agent_id)}
|
||||||
|
seat={seat}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 「新建阶段」落区(点击新增 / 拖席位到此独立成最后阶段)────────────────
|
||||||
|
|
||||||
|
function NewStageDropZone({
|
||||||
|
hasEmptyStage,
|
||||||
|
onAddStage,
|
||||||
|
}: {
|
||||||
|
hasEmptyStage: boolean;
|
||||||
|
onAddStage: () => void;
|
||||||
|
}) {
|
||||||
|
const { setNodeRef, isOver } = useDroppable({ id: NEWSTAGE_DROPPABLE_ID });
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`mat-stage-dropzone${isOver ? " is-over" : ""}`}
|
||||||
|
disabled={hasEmptyStage}
|
||||||
|
onClick={onAddStage}
|
||||||
|
ref={setNodeRef}
|
||||||
|
title={hasEmptyStage ? "已有空阶段,先填充它" : "新建一个串在最后的阶段"}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Plus size={15} />
|
||||||
|
新建阶段(串在最后) · 或把席位拖到此处独立成阶段
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 页面 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export default function TaskEditPage({
|
||||||
|
mode,
|
||||||
|
taskId,
|
||||||
|
}: {
|
||||||
|
mode: "create" | "edit";
|
||||||
|
taskId: string | null;
|
||||||
|
}) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { tasks, isLoading: tasksLoading } = useMultiAgentTasks();
|
||||||
|
const actions = useMultiAgentTaskActions();
|
||||||
|
const [poolSearch, setPoolSearch] = useState("");
|
||||||
|
const { agents, isLoading: agentsLoading } = useAgents({
|
||||||
|
search: poolSearch.trim() || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [schedule, setSchedule] = useState<ScheduleConfig>(defaultScheduleConfig());
|
||||||
|
const [enabled, setEnabled] = useState(true);
|
||||||
|
const [prompt, setPrompt] = useState("");
|
||||||
|
const [coordinatorPrompt, setCoordinatorPrompt] = useState("");
|
||||||
|
const [stageGroups, setStageGroups] = useState<StageGroups>([]);
|
||||||
|
const [initialized, setInitialized] = useState(false);
|
||||||
|
|
||||||
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [activeDrag, setActiveDrag] = useState<ActiveDrag>(null);
|
||||||
|
const [detailTarget, setDetailTarget] = useState<AgentDetailTarget | null>(null);
|
||||||
|
|
||||||
|
const editingTask = useMemo(
|
||||||
|
() => (mode === "edit" ? (tasks.find((task) => task.id === taskId) ?? null) : null),
|
||||||
|
[mode, taskId, tasks],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialized || mode === "create") {
|
||||||
|
setInitialized(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!tasksLoading && taskId) {
|
||||||
|
if (editingTask) {
|
||||||
|
const draft = draftFromTask(editingTask);
|
||||||
|
setName(draft.name);
|
||||||
|
setDescription(draft.description);
|
||||||
|
setSchedule(draft.schedule);
|
||||||
|
setEnabled(draft.enabled);
|
||||||
|
setPrompt(draft.prompt);
|
||||||
|
setCoordinatorPrompt(draft.coordinatorPrompt);
|
||||||
|
setStageGroups(draft.stageGroups);
|
||||||
|
}
|
||||||
|
setInitialized(true);
|
||||||
|
}
|
||||||
|
}, [editingTask, initialized, mode, taskId, tasksLoading]);
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const usedAgentIds = useMemo(
|
||||||
|
() => new Set(stageGroups.flatMap((group) => group.seats.map((seat) => seat.agent_id))),
|
||||||
|
[stageGroups],
|
||||||
|
);
|
||||||
|
const seatCount = useMemo(() => countSeats(stageGroups), [stageGroups]);
|
||||||
|
const hasEmptyStage = stageGroups.some((group) => group.seats.length === 0);
|
||||||
|
|
||||||
|
const backToList = useCallback(() => {
|
||||||
|
navigate(`${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/multi-agent-tasks`);
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
// ── 编排操作(全部复用 chain-stages 纯函数,agent_id 定位)───────────────
|
||||||
|
const addAgentAsStage = useCallback((agent: Agent) => {
|
||||||
|
if (seatCount >= MAX_SEATS) {
|
||||||
|
toast.warning(`编排最多 ${MAX_SEATS} 个智能体`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStageGroups((prev) => addSeatAsNewStage(prev, seatFromAgent(agent)));
|
||||||
|
}, [seatCount]);
|
||||||
|
|
||||||
|
const onDragStart = useCallback((event: DragStartEvent) => {
|
||||||
|
const id = String(event.active.id);
|
||||||
|
const agentId = id.startsWith(POOL_PREFIX)
|
||||||
|
? id.slice(POOL_PREFIX.length)
|
||||||
|
: id.startsWith(SEAT_PREFIX)
|
||||||
|
? id.slice(SEAT_PREFIX.length)
|
||||||
|
: null;
|
||||||
|
if (!agentId) return;
|
||||||
|
const seatName =
|
||||||
|
agents.find((agent) => agent.id === agentId)?.name ??
|
||||||
|
stageGroups
|
||||||
|
.flatMap((group) => group.seats as MatSeat[])
|
||||||
|
.find((seat) => seat.agent_id === agentId)?.name ??
|
||||||
|
agentId;
|
||||||
|
setActiveDrag({ name: seatName });
|
||||||
|
}, [agents, stageGroups]);
|
||||||
|
|
||||||
|
const onDragEnd = useCallback(
|
||||||
|
(event: DragEndEvent) => {
|
||||||
|
setActiveDrag(null);
|
||||||
|
const activeId = String(event.active.id);
|
||||||
|
const overId = event.over ? String(event.over.id) : null;
|
||||||
|
if (!overId) return;
|
||||||
|
|
||||||
|
let targetStage = -1;
|
||||||
|
let atIndex: number | undefined;
|
||||||
|
if (overId === NEWSTAGE_DROPPABLE_ID) {
|
||||||
|
targetStage = -1;
|
||||||
|
} else if (overId.startsWith(STAGE_PREFIX)) {
|
||||||
|
targetStage = Number(overId.slice(STAGE_PREFIX.length));
|
||||||
|
} else if (overId.startsWith(SEAT_PREFIX)) {
|
||||||
|
const overAgent = overId.slice(SEAT_PREFIX.length);
|
||||||
|
targetStage = findStageIndexOfSeat(stageGroups, overAgent);
|
||||||
|
atIndex = stageGroups[targetStage]?.seats.findIndex(
|
||||||
|
(seat) => seat.agent_id === overAgent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toNewStage = overId === NEWSTAGE_DROPPABLE_ID;
|
||||||
|
|
||||||
|
if (activeId.startsWith(POOL_PREFIX)) {
|
||||||
|
const agentId = activeId.slice(POOL_PREFIX.length);
|
||||||
|
if (usedAgentIds.has(agentId)) return;
|
||||||
|
const agent = agents.find((candidate) => candidate.id === agentId);
|
||||||
|
if (!agent || seatCount >= MAX_SEATS) return;
|
||||||
|
const seat = seatFromAgent(agent);
|
||||||
|
setStageGroups((prev) =>
|
||||||
|
toNewStage || targetStage < 0
|
||||||
|
? addSeatAsNewStage(prev, seat)
|
||||||
|
: addSeatToStage(prev, seat, targetStage, atIndex),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeId.startsWith(SEAT_PREFIX)) {
|
||||||
|
const agentId = activeId.slice(SEAT_PREFIX.length);
|
||||||
|
setStageGroups((prev) => {
|
||||||
|
if (toNewStage) {
|
||||||
|
const withEmpty = addEmptyStage(prev);
|
||||||
|
return moveSeatToStage(withEmpty, agentId, withEmpty.length - 1);
|
||||||
|
}
|
||||||
|
if (targetStage < 0) return prev;
|
||||||
|
return moveSeatToStage(prev, agentId, targetStage, atIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[agents, seatCount, stageGroups, usedAgentIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 保存:校验 → 确认弹窗 → 提交 ────────────────────────────────────────
|
||||||
|
const previewGroups = stageGroups.filter((group) => group.seats.length > 0);
|
||||||
|
const previewStages = previewGroups.map((group) =>
|
||||||
|
(group.seats as MatSeat[]).map((seat) => seat.agent_id),
|
||||||
|
);
|
||||||
|
const previewSeats = groupsToPayloadSeats(stageGroups);
|
||||||
|
|
||||||
|
const tryOpenConfirm = () => {
|
||||||
|
if (!name.trim()) {
|
||||||
|
toast.warning("请填写任务名称");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (seatCount < MIN_SEATS) {
|
||||||
|
toast.warning(`编排至少需要 ${MIN_SEATS} 个智能体`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hasEmptyStage) {
|
||||||
|
toast.warning("还有空阶段,请填充或删除");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!prompt.trim()) {
|
||||||
|
setSettingsOpen(true);
|
||||||
|
toast.warning("请先在「执行设置」中填写任务指令");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setConfirmOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitTask = async () => {
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
// 填了总控提示也强制落显式分层(该提示仅分层模式注入),与业务链条语义一致。
|
||||||
|
const forceExplicit = coordinatorPrompt.trim().length > 0;
|
||||||
|
const payload: MatTaskPayload = {
|
||||||
|
name: name.trim(),
|
||||||
|
description: description.trim() || undefined,
|
||||||
|
prompt: prompt.trim(),
|
||||||
|
coordinator_prompt: coordinatorPrompt.trim() || undefined,
|
||||||
|
cron_expr: buildCron(schedule),
|
||||||
|
timezone: editingTask?.timezone || DEFAULT_TIMEZONE,
|
||||||
|
enabled,
|
||||||
|
seats: previewSeats,
|
||||||
|
stages: stageGroupsToStagePayload(stageGroups, forceExplicit),
|
||||||
|
stage_goals: stageGroupsToStageGoals(stageGroups, forceExplicit),
|
||||||
|
};
|
||||||
|
if (mode === "edit" && taskId) {
|
||||||
|
await actions.update.mutateAsync({ id: taskId, payload });
|
||||||
|
toast.success("任务已更新");
|
||||||
|
} else {
|
||||||
|
await actions.create.mutateAsync(payload);
|
||||||
|
toast.success("任务已创建");
|
||||||
|
}
|
||||||
|
setConfirmOpen(false);
|
||||||
|
backToList();
|
||||||
|
} catch {
|
||||||
|
toast.error("保存失败,请重试");
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mode === "edit" && !tasksLoading && initialized && !editingTask) {
|
||||||
|
return (
|
||||||
|
<section className="mat-edit-page">
|
||||||
|
<div className="mat-state mat-state--empty">
|
||||||
|
<h3>任务不存在或已被删除</h3>
|
||||||
|
<Button theme="primary" onClick={backToList}>
|
||||||
|
返回任务列表
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let seatCursor = 0;
|
||||||
|
const settingsSummary = `${describeScheduleConfig(schedule)} · ${enabled ? "已启用" : "已暂停"}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="mat-edit-page">
|
||||||
|
<header className="mat-edit-header">
|
||||||
|
<button aria-label="返回任务列表" className="mat-edit-back" onClick={backToList} type="button">
|
||||||
|
<ArrowLeft size={18} />
|
||||||
|
</button>
|
||||||
|
<div className="mat-edit-heading">
|
||||||
|
<h1>{mode === "edit" ? `编辑:${editingTask?.name ?? "多智能体定时任务"}` : "新建多智能体定时任务"}</h1>
|
||||||
|
<p>同阶段并行执行 · 阶段之间串行执行;拖拽或点击左侧候选智能体加入编排</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setSettingsOpen(true)}
|
||||||
|
title="执行频率 / 任务指令 / 总控提示 / 启用"
|
||||||
|
>
|
||||||
|
<Settings2 aria-hidden="true" size={15} />
|
||||||
|
执行设置
|
||||||
|
<span className="mat-settings-summary">{settingsSummary}</span>
|
||||||
|
</Button>
|
||||||
|
<Button theme="primary" onClick={tryOpenConfirm}>
|
||||||
|
<Save aria-hidden="true" size={15} />
|
||||||
|
保存任务
|
||||||
|
</Button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="mat-edit-formbar">
|
||||||
|
<label className="mat-formbar-field">
|
||||||
|
<span className="mat-formbar-label">
|
||||||
|
任务名称<i>*</i>
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
clearable
|
||||||
|
maxlength={40}
|
||||||
|
placeholder="例如:每日舆情早报"
|
||||||
|
value={name}
|
||||||
|
onChange={(value) => setName(String(value ?? ""))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="mat-formbar-field mat-formbar-field--wide">
|
||||||
|
<span className="mat-formbar-label">任务描述</span>
|
||||||
|
<Input
|
||||||
|
clearable
|
||||||
|
maxlength={200}
|
||||||
|
placeholder="一句话说明这个编排任务要达成什么(可选)"
|
||||||
|
value={description}
|
||||||
|
onChange={(value) => setDescription(String(value ?? ""))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mat-edit-formbar mat-edit-formbar--area">
|
||||||
|
<span className="mat-formbar-label">总控提示</span>
|
||||||
|
<Textarea
|
||||||
|
autosize={{ minRows: 1, maxRows: 4 }}
|
||||||
|
className="mat-formbar-textarea"
|
||||||
|
maxlength={1000}
|
||||||
|
placeholder="写给总控(协调者)的编排要求,例如各阶段如何衔接、产出格式约束等(可选)"
|
||||||
|
value={coordinatorPrompt}
|
||||||
|
onChange={(value) => setCoordinatorPrompt(String(value ?? ""))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mat-edit-body">
|
||||||
|
<section className="mat-edit-card">
|
||||||
|
<DndContext
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
sensors={sensors}
|
||||||
|
>
|
||||||
|
<div className="mat-edit-orchestra">
|
||||||
|
<aside className="mat-pool">
|
||||||
|
<header className="mat-pool-head">
|
||||||
|
<span>候选智能体</span>
|
||||||
|
<em>
|
||||||
|
{seatCount}/{MAX_SEATS} 已编排 · {agents.length} 个可选
|
||||||
|
</em>
|
||||||
|
</header>
|
||||||
|
<div className="mat-pool-search">
|
||||||
|
<Search aria-hidden="true" size={15} />
|
||||||
|
<input
|
||||||
|
onChange={(event) => setPoolSearch(event.target.value)}
|
||||||
|
placeholder="搜索智能体"
|
||||||
|
value={poolSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mat-pool-list">
|
||||||
|
{agentsLoading ? (
|
||||||
|
<p className="mat-pool-tip">正在加载智能体…</p>
|
||||||
|
) : agents.length ? (
|
||||||
|
agents.map((agent) => (
|
||||||
|
<PoolAgentCard
|
||||||
|
agent={agent}
|
||||||
|
key={agent.id}
|
||||||
|
onAdd={() => addAgentAsStage(agent)}
|
||||||
|
onShowDetail={() =>
|
||||||
|
setDetailTarget({
|
||||||
|
agentId: agent.id,
|
||||||
|
fallbackName: agent.name,
|
||||||
|
fallbackDescription: agent.description ?? undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
used={usedAgentIds.has(agent.id)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="mat-pool-tip">没有匹配的智能体</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div className="mat-stage-editor">
|
||||||
|
<div className="mat-stage-list">
|
||||||
|
{stageGroups.map((group, stageIdx) => {
|
||||||
|
const startIndex = seatCursor;
|
||||||
|
seatCursor += (group.seats as MatSeat[]).length;
|
||||||
|
return (
|
||||||
|
<StageBox
|
||||||
|
group={group}
|
||||||
|
key={`stage-box-${stageIdx}`}
|
||||||
|
onGoalChange={(goal) =>
|
||||||
|
setStageGroups((prev) => setStageGoal(prev, stageIdx, goal))
|
||||||
|
}
|
||||||
|
onMergeSeat={(agentId, target) =>
|
||||||
|
setStageGroups((prev) => moveSeatToStage(prev, agentId, target))
|
||||||
|
}
|
||||||
|
onRemoveSeat={(agentId) =>
|
||||||
|
setStageGroups((prev) => removeSeat(prev, agentId))
|
||||||
|
}
|
||||||
|
onSeatDetail={(seat) =>
|
||||||
|
setDetailTarget({
|
||||||
|
agentId: seat.agent_id,
|
||||||
|
fallbackName: seat.name,
|
||||||
|
fallbackDescription: seat.description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onSplitSeat={(agentId) =>
|
||||||
|
setStageGroups((prev) => splitSeatToNewStage(prev, agentId))
|
||||||
|
}
|
||||||
|
onStageDelete={() =>
|
||||||
|
setStageGroups((prev) => deleteStage(prev, stageIdx))
|
||||||
|
}
|
||||||
|
onStageReorder={(dir) =>
|
||||||
|
setStageGroups((prev) => moveStage(prev, stageIdx, dir))
|
||||||
|
}
|
||||||
|
stageCount={stageGroups.length}
|
||||||
|
stageIdx={stageIdx}
|
||||||
|
startIndex={startIndex}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<NewStageDropZone
|
||||||
|
hasEmptyStage={hasEmptyStage}
|
||||||
|
onAddStage={() => setStageGroups((prev) => addEmptyStage(prev))}
|
||||||
|
/>
|
||||||
|
{!stageGroups.length && (
|
||||||
|
<p className="mat-stage-empty">
|
||||||
|
从左侧点击或拖拽候选智能体加入编排,至少 {MIN_SEATS} 个。
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DragOverlay>
|
||||||
|
{activeDrag ? (
|
||||||
|
<div className="mat-drag-overlay">
|
||||||
|
<GripVertical size={14} />
|
||||||
|
{activeDrag.name}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</DragOverlay>
|
||||||
|
</DndContext>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
className="mat-settings-dialog"
|
||||||
|
closeBtn
|
||||||
|
closeOnOverlayClick={false}
|
||||||
|
destroyOnClose
|
||||||
|
footer={
|
||||||
|
<div className="mat-confirm-footer">
|
||||||
|
<Button variant="outline" onClick={() => setSettingsOpen(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button theme="primary" onClick={() => setSettingsOpen(false)}>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
header="执行设置"
|
||||||
|
placement="top"
|
||||||
|
visible={settingsOpen}
|
||||||
|
width={620}
|
||||||
|
onClose={() => setSettingsOpen(false)}
|
||||||
|
>
|
||||||
|
<div className="mat-settings">
|
||||||
|
<div className="mat-field">
|
||||||
|
<span className="mat-field-label">执行频率</span>
|
||||||
|
<SchedulePicker value={schedule} onChange={setSchedule} />
|
||||||
|
</div>
|
||||||
|
<label className="mat-field">
|
||||||
|
<span className="mat-field-label">
|
||||||
|
任务指令<i>*</i>
|
||||||
|
</span>
|
||||||
|
<Textarea
|
||||||
|
autosize={{ minRows: 4, maxRows: 8 }}
|
||||||
|
maxlength={2000}
|
||||||
|
placeholder="描述每次定时执行要让智能体编队完成的事情,例如:汇总过去 24 小时重点舆情并生成早报…"
|
||||||
|
value={prompt}
|
||||||
|
onChange={(value) => setPrompt(String(value ?? ""))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="mat-field mat-field--switch">
|
||||||
|
<span className="mat-field-label">保存后立即启用</span>
|
||||||
|
<Switch value={enabled} onChange={(value) => setEnabled(!!value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
className="mat-confirm-dialog"
|
||||||
|
closeBtn
|
||||||
|
closeOnOverlayClick={false}
|
||||||
|
destroyOnClose
|
||||||
|
footer={
|
||||||
|
<div className="mat-confirm-footer">
|
||||||
|
<Button variant="outline" onClick={() => setConfirmOpen(false)}>
|
||||||
|
再改改
|
||||||
|
</Button>
|
||||||
|
<Button loading={saving} theme="primary" onClick={() => void submitTask()}>
|
||||||
|
确认保存
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
header="确认保存任务"
|
||||||
|
placement="top"
|
||||||
|
visible={confirmOpen}
|
||||||
|
width={720}
|
||||||
|
onClose={() => setConfirmOpen(false)}
|
||||||
|
>
|
||||||
|
<div className="mat-confirm">
|
||||||
|
<div className="mat-confirm-grid">
|
||||||
|
<div>
|
||||||
|
<span>任务名称</span>
|
||||||
|
<strong>{name.trim() || "—"}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>执行频率</span>
|
||||||
|
<strong>{describeCron(buildCron(schedule))}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>启用状态</span>
|
||||||
|
<strong>{enabled ? "启用" : "暂停"}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>智能体 / 阶段</span>
|
||||||
|
<strong>
|
||||||
|
{previewSeats.length} 个智能体 · {previewStages.length} 个阶段
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mat-confirm-flow">
|
||||||
|
<AgentFlow
|
||||||
|
seats={previewSeats}
|
||||||
|
showGoals
|
||||||
|
size="sm"
|
||||||
|
stageGoals={previewGroups.map((group) => (group.goal ?? "").trim() || null)}
|
||||||
|
stages={previewStages}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="mat-confirm-legend">
|
||||||
|
<span>
|
||||||
|
<i className="mat-flow-mode is-parallel" style={{ cursor: "default" }} /> 并行:同阶段智能体同时执行
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<i className="mat-flow-mode is-serial" style={{ cursor: "default" }} /> 串行:阶段之间依次执行
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
{prompt.trim() && (
|
||||||
|
<div className="mat-confirm-prompt">
|
||||||
|
<span>任务指令</span>
|
||||||
|
<p>{prompt.trim()}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<AgentDetailDrawer
|
||||||
|
target={detailTarget}
|
||||||
|
onClose={() => setDetailTarget(null)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* 智能体详情抽屉:候选池卡片 / 编排席位条「查看详情」共用。
|
||||||
|
* 后端查不到(本地演示席位)时回退到编排里保存的名称/描述快照。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Bot } from "lucide-react";
|
||||||
|
import { Drawer, Tag } from "tdesign-react";
|
||||||
|
|
||||||
|
import { useAgent } from "@/core/agents";
|
||||||
|
|
||||||
|
export interface AgentDetailTarget {
|
||||||
|
agentId: string;
|
||||||
|
fallbackName?: string;
|
||||||
|
fallbackDescription?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value: string | null | undefined): string {
|
||||||
|
if (!value) return "—";
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return "—";
|
||||||
|
return new Intl.DateTimeFormat("zh-CN", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
}).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AgentDetailDrawer({
|
||||||
|
target,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
target: AgentDetailTarget | null;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const { agent, isLoading } = useAgent(target?.agentId ?? null, {
|
||||||
|
enabled: !!target,
|
||||||
|
});
|
||||||
|
|
||||||
|
const name = agent?.name ?? target?.fallbackName ?? target?.agentId ?? "";
|
||||||
|
const description =
|
||||||
|
agent?.description ?? target?.fallbackDescription ?? agent?.description ?? "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
className="mat-agent-drawer"
|
||||||
|
closeBtn
|
||||||
|
destroyOnClose
|
||||||
|
footer={null}
|
||||||
|
header={
|
||||||
|
<div className="mat-agent-header">
|
||||||
|
<span className="mat-agent-glyph" aria-hidden="true">
|
||||||
|
<Bot size={17} />
|
||||||
|
</span>
|
||||||
|
<div className="mat-agent-heading">
|
||||||
|
<h3>{name}</h3>
|
||||||
|
<p>智能体详情</p>
|
||||||
|
</div>
|
||||||
|
{agent && (
|
||||||
|
<Tag size="small" theme={agent.builtin ? "default" : "primary"} variant="light">
|
||||||
|
{agent.builtin ? "内置" : agent.published ? "已发布" : "私有"}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
placement="right"
|
||||||
|
size="460px"
|
||||||
|
visible={!!target}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
{target && (
|
||||||
|
<div className="mat-agent">
|
||||||
|
{isLoading ? (
|
||||||
|
<p className="mat-agent-loading">正在加载智能体详情…</p>
|
||||||
|
) : agent ? (
|
||||||
|
<>
|
||||||
|
<div className="mat-detail-grid">
|
||||||
|
<div>
|
||||||
|
<span>创建者</span>
|
||||||
|
<strong>
|
||||||
|
{agent.owner_username
|
||||||
|
? agent.owner_username
|
||||||
|
: agent.builtin
|
||||||
|
? "内置智能体"
|
||||||
|
: "—"}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>执行模型</span>
|
||||||
|
<strong>{agent.model || "默认模型"}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>创建时间</span>
|
||||||
|
<strong>{formatDateTime(agent.created_at)}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>技能数</span>
|
||||||
|
<strong>{agent.skills?.length ?? 0} 个</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{agent.description && (
|
||||||
|
<section className="mat-agent-section">
|
||||||
|
<header className="mat-detail-section-head">
|
||||||
|
<h4>简介</h4>
|
||||||
|
</header>
|
||||||
|
<p className="mat-agent-desc">{agent.description}</p>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!!agent.tags?.length && (
|
||||||
|
<section className="mat-agent-section">
|
||||||
|
<header className="mat-detail-section-head">
|
||||||
|
<h4>标签</h4>
|
||||||
|
</header>
|
||||||
|
<div className="mat-agent-tags">
|
||||||
|
{agent.tags.map((tag) => (
|
||||||
|
<Tag key={tag.id} size="small" variant="light">
|
||||||
|
{tag.name}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!!agent.skills?.length && (
|
||||||
|
<section className="mat-agent-section">
|
||||||
|
<header className="mat-detail-section-head">
|
||||||
|
<h4>已配置技能</h4>
|
||||||
|
</header>
|
||||||
|
<div className="mat-agent-tags">
|
||||||
|
{agent.skills.map((skill) => (
|
||||||
|
<Tag key={skill} size="small" theme="primary" variant="light">
|
||||||
|
{skill}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="mat-agent-local">
|
||||||
|
<div className="mat-detail-grid">
|
||||||
|
<div>
|
||||||
|
<span>创建者</span>
|
||||||
|
<strong>本地演示</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>数据来源</span>
|
||||||
|
<strong>编排内置快照</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="mat-agent-desc">
|
||||||
|
{description || "该席位来自演示种子数据,未对应后端智能体目录中的记录。接入后端后,编排将引用真实智能体。"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MoveRight } from "lucide-react";
|
import { Bot, MoveRight } from "lucide-react";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import type { MatSeat, MatSeatRunState } from "../types";
|
import type { MatSeat, MatSeatRunState } from "../types";
|
||||||
@@ -23,12 +23,6 @@ const STATE_LABEL: Record<MatSeatRunState, string> = {
|
|||||||
failed: "失败",
|
failed: "失败",
|
||||||
};
|
};
|
||||||
|
|
||||||
function initialsOf(name: string): string {
|
|
||||||
const trimmed = name.trim();
|
|
||||||
if (!trimmed) return "?";
|
|
||||||
return trimmed.slice(0, 2).toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AgentFlow({
|
export default function AgentFlow({
|
||||||
stages,
|
stages,
|
||||||
seats,
|
seats,
|
||||||
@@ -88,7 +82,9 @@ export default function AgentFlow({
|
|||||||
: (seat?.name ?? agentId)
|
: (seat?.name ?? agentId)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="mat-seat-avatar">{initialsOf(seat?.name ?? agentId)}</span>
|
<span className="mat-seat-avatar">
|
||||||
|
<Bot size={size === "sm" ? 11 : 12} strokeWidth={2.2} />
|
||||||
|
</span>
|
||||||
<span className="mat-seat-name">{seat?.name ?? agentId}</span>
|
<span className="mat-seat-name">{seat?.name ?? agentId}</span>
|
||||||
{state && (
|
{state && (
|
||||||
<span className={`mat-seat-dot is-${state}`} title={STATE_LABEL[state]} />
|
<span className={`mat-seat-dot is-${state}`} title={STATE_LABEL[state]} />
|
||||||
|
|||||||
@@ -1,571 +0,0 @@
|
|||||||
import {
|
|
||||||
ArrowDown,
|
|
||||||
ArrowUp,
|
|
||||||
ChevronDown,
|
|
||||||
ChevronUp,
|
|
||||||
Plus,
|
|
||||||
Search,
|
|
||||||
SplitSquareVertical,
|
|
||||||
Trash2,
|
|
||||||
X,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
Input,
|
|
||||||
MessagePlugin,
|
|
||||||
Select,
|
|
||||||
Steps,
|
|
||||||
Switch,
|
|
||||||
Textarea,
|
|
||||||
} from "tdesign-react";
|
|
||||||
|
|
||||||
import { useAgents } from "@/core/agents";
|
|
||||||
import {
|
|
||||||
type ScheduleConfig,
|
|
||||||
buildCron,
|
|
||||||
parseCron,
|
|
||||||
} from "@/components/workspace/scheduled-tasks/schedule-builder";
|
|
||||||
|
|
||||||
import AgentFlow from "./AgentFlow";
|
|
||||||
import SchedulePicker from "./SchedulePicker";
|
|
||||||
import type { MatSeat, MatStageGroup, MatTask, MatTaskPayload } from "../types";
|
|
||||||
import { defaultScheduleConfig, expandStages } from "../utils";
|
|
||||||
|
|
||||||
const TIMEZONE_OPTIONS = ["Asia/Shanghai", "UTC", "America/New_York", "Europe/London"].map(
|
|
||||||
(zone) => ({ label: zone, value: zone }),
|
|
||||||
);
|
|
||||||
|
|
||||||
const MAX_SEATS = 20;
|
|
||||||
const MIN_SEATS = 2;
|
|
||||||
|
|
||||||
interface EditorDraft {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
schedule: ScheduleConfig;
|
|
||||||
timezone: string;
|
|
||||||
enabled: boolean;
|
|
||||||
stageGroups: MatStageGroup[];
|
|
||||||
prompt: string;
|
|
||||||
coordinatorPrompt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function draftFromTask(task: MatTask | null): EditorDraft {
|
|
||||||
if (!task) {
|
|
||||||
return {
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
schedule: defaultScheduleConfig(),
|
|
||||||
timezone: "Asia/Shanghai",
|
|
||||||
enabled: true,
|
|
||||||
stageGroups: [],
|
|
||||||
prompt: "",
|
|
||||||
coordinatorPrompt: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
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(),
|
|
||||||
timezone: task.timezone || "Asia/Shanghai",
|
|
||||||
enabled: task.enabled,
|
|
||||||
stageGroups: stages.map((stage, index) => ({
|
|
||||||
seats: stage
|
|
||||||
.map((agentId) => seatById.get(agentId))
|
|
||||||
.filter((seat): seat is MatSeat => !!seat),
|
|
||||||
goal: task.stage_goals?.[index] ?? "",
|
|
||||||
})),
|
|
||||||
prompt: task.prompt,
|
|
||||||
coordinatorPrompt: task.coordinator_prompt ?? "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function draftToPayload(draft: EditorDraft): MatTaskPayload {
|
|
||||||
const groups = draft.stageGroups.filter((group) => group.seats.length > 0);
|
|
||||||
const seats = groups.flatMap((group) => group.seats);
|
|
||||||
const anyParallel = groups.some((group) => group.seats.length > 1);
|
|
||||||
const anyGoal = groups.some((group) => group.goal.trim().length > 0);
|
|
||||||
const explicit = anyParallel || anyGoal;
|
|
||||||
return {
|
|
||||||
name: draft.name.trim(),
|
|
||||||
description: draft.description.trim() || undefined,
|
|
||||||
prompt: draft.prompt.trim(),
|
|
||||||
coordinator_prompt: draft.coordinatorPrompt.trim() || undefined,
|
|
||||||
cron_expr: buildCron(draft.schedule),
|
|
||||||
timezone: draft.timezone,
|
|
||||||
enabled: draft.enabled,
|
|
||||||
seats,
|
|
||||||
stages: explicit ? groups.map((group) => group.seats.map((seat) => seat.agent_id)) : null,
|
|
||||||
stage_goals: explicit ? groups.map((group) => group.goal.trim() || null) : null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TaskEditorDialog({
|
|
||||||
visible,
|
|
||||||
task,
|
|
||||||
submitting,
|
|
||||||
onClose,
|
|
||||||
onSubmit,
|
|
||||||
}: {
|
|
||||||
visible: boolean;
|
|
||||||
task: MatTask | null;
|
|
||||||
submitting: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onSubmit: (payload: MatTaskPayload) => void;
|
|
||||||
}) {
|
|
||||||
const [step, setStep] = useState(0);
|
|
||||||
const [draft, setDraft] = useState<EditorDraft>(() => draftFromTask(task));
|
|
||||||
const [poolSearch, setPoolSearch] = useState("");
|
|
||||||
const [stageAddValues, setStageAddValues] = useState<Record<number, string | undefined>>({});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible) {
|
|
||||||
setStep(0);
|
|
||||||
setPoolSearch("");
|
|
||||||
setDraft(draftFromTask(task));
|
|
||||||
}
|
|
||||||
}, [visible, task]);
|
|
||||||
|
|
||||||
const { agents, isLoading: agentsLoading } = useAgents({ search: poolSearch.trim() || undefined });
|
|
||||||
|
|
||||||
const usedAgentIds = useMemo(
|
|
||||||
() => new Set(draft.stageGroups.flatMap((group) => group.seats.map((seat) => seat.agent_id))),
|
|
||||||
[draft.stageGroups],
|
|
||||||
);
|
|
||||||
const seatTotal = usedAgentIds.size;
|
|
||||||
|
|
||||||
const updateDraft = (patch: Partial<EditorDraft>) => setDraft((prev) => ({ ...prev, ...patch }));
|
|
||||||
const updateGroups = (
|
|
||||||
updater: (groups: MatStageGroup[]) => MatStageGroup[],
|
|
||||||
) => updateDraft({ stageGroups: updater(draft.stageGroups) });
|
|
||||||
|
|
||||||
const addAgentAsNewStage = (seat: MatSeat) => {
|
|
||||||
if (usedAgentIds.has(seat.agent_id) || seatTotal >= MAX_SEATS) return;
|
|
||||||
updateGroups((groups) => [...groups, { seats: [seat], goal: "" }]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addAgentToStage = (seat: MatSeat, stageIndex: number) => {
|
|
||||||
if (usedAgentIds.has(seat.agent_id) || seatTotal >= MAX_SEATS) return;
|
|
||||||
updateGroups((groups) =>
|
|
||||||
groups.map((group, index) =>
|
|
||||||
index === stageIndex ? { ...group, seats: [...group.seats, seat] } : group,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeSeat = (stageIndex: number, seatIndex: number) => {
|
|
||||||
updateGroups((groups) =>
|
|
||||||
groups
|
|
||||||
.map((group, index) =>
|
|
||||||
index === stageIndex
|
|
||||||
? { ...group, seats: group.seats.filter((_, seatIdx) => seatIdx !== seatIndex) }
|
|
||||||
: group,
|
|
||||||
)
|
|
||||||
.filter((group) => group.seats.length > 0),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveSeatAcrossStage = (stageIndex: number, seatIndex: number, delta: 1 | -1) => {
|
|
||||||
const target = stageIndex + delta;
|
|
||||||
if (target < 0 || target >= draft.stageGroups.length) return;
|
|
||||||
updateGroups((groups) => {
|
|
||||||
const next = groups.map((group) => ({ seats: [...group.seats], goal: group.goal }));
|
|
||||||
const seat = next[stageIndex]!.seats[seatIndex];
|
|
||||||
if (!seat) return groups;
|
|
||||||
next[stageIndex]!.seats.splice(seatIndex, 1);
|
|
||||||
next[target]!.seats.push(seat);
|
|
||||||
return next.filter((group) => group.seats.length > 0);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const splitSeatToStage = (stageIndex: number, seatIndex: number) => {
|
|
||||||
updateGroups((groups) => {
|
|
||||||
const next = groups.map((group) => ({ seats: [...group.seats], goal: group.goal }));
|
|
||||||
const seat = next[stageIndex]!.seats[seatIndex];
|
|
||||||
if (!seat) return groups;
|
|
||||||
next[stageIndex]!.seats.splice(seatIndex, 1);
|
|
||||||
next.splice(stageIndex + 1, 0, { seats: [seat], goal: "" });
|
|
||||||
return next.filter((group) => group.seats.length > 0);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveStage = (stageIndex: number, delta: 1 | -1) => {
|
|
||||||
const target = stageIndex + delta;
|
|
||||||
if (target < 0 || target >= draft.stageGroups.length) return;
|
|
||||||
updateGroups((groups) => {
|
|
||||||
const next = [...groups];
|
|
||||||
const [group] = next.splice(stageIndex, 1);
|
|
||||||
if (!group) return groups;
|
|
||||||
next.splice(target, 0, group);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteStage = (stageIndex: number) => {
|
|
||||||
updateGroups((groups) => groups.filter((_, index) => index !== stageIndex));
|
|
||||||
};
|
|
||||||
|
|
||||||
const setStageGoal = (stageIndex: number, goal: string) => {
|
|
||||||
updateGroups((groups) =>
|
|
||||||
groups.map((group, index) => (index === stageIndex ? { ...group, goal } : group)),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const trySubmit = () => {
|
|
||||||
if (!draft.name.trim()) {
|
|
||||||
setStep(0);
|
|
||||||
void MessagePlugin.warning("请填写任务名称");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (seatTotal < MIN_SEATS) {
|
|
||||||
setStep(1);
|
|
||||||
void MessagePlugin.warning(`编排至少需要 ${MIN_SEATS} 个智能体`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!draft.prompt.trim()) {
|
|
||||||
setStep(2);
|
|
||||||
void MessagePlugin.warning("请填写任务指令");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onSubmit(draftToPayload(draft));
|
|
||||||
};
|
|
||||||
|
|
||||||
const previewStages = draft.stageGroups
|
|
||||||
.filter((group) => group.seats.length > 0)
|
|
||||||
.map((group) => group.seats.map((seat) => seat.agent_id));
|
|
||||||
const previewSeats = draft.stageGroups.flatMap((group) => group.seats);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
className="mat-editor-dialog"
|
|
||||||
closeBtn
|
|
||||||
closeOnOverlayClick={false}
|
|
||||||
destroyOnClose
|
|
||||||
footer={null}
|
|
||||||
header={task ? "编辑多智能体定时任务" : "新建多智能体定时任务"}
|
|
||||||
placement="top"
|
|
||||||
visible={visible}
|
|
||||||
width={980}
|
|
||||||
onClose={onClose}
|
|
||||||
>
|
|
||||||
<div className="mat-editor">
|
|
||||||
<Steps className="mat-editor-steps" current={step} readonly>
|
|
||||||
<Steps.StepItem title="基本信息" />
|
|
||||||
<Steps.StepItem title="智能体编排" />
|
|
||||||
<Steps.StepItem title="执行设置" />
|
|
||||||
</Steps>
|
|
||||||
|
|
||||||
{step === 0 && (
|
|
||||||
<div className="mat-editor-pane">
|
|
||||||
<label className="mat-field">
|
|
||||||
<span className="mat-field-label">
|
|
||||||
任务名称<i>*</i>
|
|
||||||
</span>
|
|
||||||
<Input
|
|
||||||
clearable
|
|
||||||
maxlength={40}
|
|
||||||
placeholder="例如:每日舆情早报"
|
|
||||||
value={draft.name}
|
|
||||||
onChange={(value) => updateDraft({ name: String(value ?? "") })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="mat-field">
|
|
||||||
<span className="mat-field-label">任务描述</span>
|
|
||||||
<Textarea
|
|
||||||
autosize={{ minRows: 2, maxRows: 4 }}
|
|
||||||
maxlength={200}
|
|
||||||
placeholder="一句话说明这个编排任务要达成什么(可选)"
|
|
||||||
value={draft.description}
|
|
||||||
onChange={(value) => updateDraft({ description: String(value ?? "") })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div className="mat-field">
|
|
||||||
<span className="mat-field-label">执行频率</span>
|
|
||||||
<SchedulePicker
|
|
||||||
value={draft.schedule}
|
|
||||||
onChange={(schedule) => updateDraft({ schedule })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mat-field-row">
|
|
||||||
<label className="mat-field">
|
|
||||||
<span className="mat-field-label">时区</span>
|
|
||||||
<Select
|
|
||||||
options={TIMEZONE_OPTIONS}
|
|
||||||
value={draft.timezone}
|
|
||||||
onChange={(value) => updateDraft({ timezone: String(value) })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="mat-field mat-field--switch">
|
|
||||||
<span className="mat-field-label">创建后立即启用</span>
|
|
||||||
<Switch value={draft.enabled} onChange={(value) => updateDraft({ enabled: !!value })} />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 1 && (
|
|
||||||
<div className="mat-editor-pane mat-editor-pane--wide">
|
|
||||||
<aside className="mat-pool">
|
|
||||||
<header className="mat-pool-head">
|
|
||||||
<span>候选智能体</span>
|
|
||||||
<em>
|
|
||||||
{seatTotal} / {MAX_SEATS}
|
|
||||||
</em>
|
|
||||||
</header>
|
|
||||||
<div className="mat-pool-search">
|
|
||||||
<Search aria-hidden="true" size={15} />
|
|
||||||
<input
|
|
||||||
onChange={(event) => setPoolSearch(event.target.value)}
|
|
||||||
placeholder="搜索智能体"
|
|
||||||
value={poolSearch}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mat-pool-list">
|
|
||||||
{agentsLoading ? (
|
|
||||||
<p className="mat-pool-tip">正在加载智能体…</p>
|
|
||||||
) : agents.length ? (
|
|
||||||
agents.map((agent) => {
|
|
||||||
const used = usedAgentIds.has(agent.id);
|
|
||||||
const seat: MatSeat = {
|
|
||||||
agent_id: agent.id,
|
|
||||||
name: agent.name,
|
|
||||||
description: agent.description ?? undefined,
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="mat-pool-item"
|
|
||||||
disabled={used || seatTotal >= MAX_SEATS}
|
|
||||||
key={agent.id}
|
|
||||||
onClick={() => addAgentAsNewStage(seat)}
|
|
||||||
title={used ? "该智能体已在编排中" : "点击追加为新阶段"}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span className="mat-pool-item-name">{agent.name}</span>
|
|
||||||
<span className="mat-pool-item-desc">
|
|
||||||
{agent.description || "暂无简介"}
|
|
||||||
</span>
|
|
||||||
<span className="mat-pool-item-add" aria-hidden="true">
|
|
||||||
<Plus size={14} />
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<p className="mat-pool-tip">没有匹配的智能体</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<section className="mat-stage-editor">
|
|
||||||
<header className="mat-stage-editor-head">
|
|
||||||
<span>阶段编排</span>
|
|
||||||
<small>同阶段并行执行 · 阶段之间串行;点击候选智能体追加为新阶段</small>
|
|
||||||
</header>
|
|
||||||
<div className="mat-stage-list">
|
|
||||||
{draft.stageGroups.map((group, stageIndex) => {
|
|
||||||
const unusedOptions = agents
|
|
||||||
.filter((agent) => !usedAgentIds.has(agent.id))
|
|
||||||
.map((agent) => ({ label: agent.name, value: agent.id, agent }));
|
|
||||||
return (
|
|
||||||
<article className="mat-stage-box" key={`stage-box-${stageIndex}`}>
|
|
||||||
<header className="mat-stage-box-head">
|
|
||||||
<span className="mat-stage-box-no">阶段 {stageIndex + 1}</span>
|
|
||||||
{group.seats.length > 1 ? (
|
|
||||||
<span className="mat-flow-mode is-parallel">并行 ×{group.seats.length}</span>
|
|
||||||
) : (
|
|
||||||
<span className="mat-flow-mode is-serial">串行</span>
|
|
||||||
)}
|
|
||||||
<span className="mat-stage-box-ops">
|
|
||||||
<button
|
|
||||||
aria-label="阶段上移"
|
|
||||||
disabled={stageIndex === 0}
|
|
||||||
onClick={() => moveStage(stageIndex, -1)}
|
|
||||||
title="上移"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<ArrowUp size={14} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
aria-label="阶段下移"
|
|
||||||
disabled={stageIndex === draft.stageGroups.length - 1}
|
|
||||||
onClick={() => moveStage(stageIndex, 1)}
|
|
||||||
title="下移"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<ArrowDown size={14} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
aria-label="删除阶段"
|
|
||||||
onClick={() => deleteStage(stageIndex)}
|
|
||||||
title="删除整个阶段"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Trash2 size={14} />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</header>
|
|
||||||
<Textarea
|
|
||||||
autosize
|
|
||||||
className="mat-stage-goal"
|
|
||||||
maxlength={120}
|
|
||||||
placeholder="本阶段目标(可选):告诉总控本阶段要产出什么、如何衔接上一阶段"
|
|
||||||
value={group.goal}
|
|
||||||
onChange={(value) => setStageGoal(stageIndex, String(value ?? ""))}
|
|
||||||
/>
|
|
||||||
<div className="mat-stage-seats">
|
|
||||||
{group.seats.map((seat, seatIndex) => (
|
|
||||||
<div className="mat-seat-row" key={seat.agent_id}>
|
|
||||||
<span className="mat-seat-row-name" title={seat.description}>
|
|
||||||
{seat.name}
|
|
||||||
</span>
|
|
||||||
<span className="mat-seat-row-ops">
|
|
||||||
<button
|
|
||||||
aria-label="并入上一阶段(与其并行)"
|
|
||||||
disabled={stageIndex === 0}
|
|
||||||
onClick={() => moveSeatAcrossStage(stageIndex, seatIndex, -1)}
|
|
||||||
title="并入上一阶段(与其并行)"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<ChevronUp size={14} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
aria-label="独立成阶段"
|
|
||||||
onClick={() => splitSeatToStage(stageIndex, seatIndex)}
|
|
||||||
title="独立成阶段"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<SplitSquareVertical size={14} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
aria-label="并入下一阶段(与其并行)"
|
|
||||||
disabled={stageIndex === draft.stageGroups.length - 1}
|
|
||||||
onClick={() => moveSeatAcrossStage(stageIndex, seatIndex, 1)}
|
|
||||||
title="并入下一阶段(与其并行)"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<ChevronDown size={14} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
aria-label="移除智能体"
|
|
||||||
onClick={() => removeSeat(stageIndex, seatIndex)}
|
|
||||||
title="移除"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<X size={14} />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{seatTotal < MAX_SEATS && unusedOptions.length > 0 && (
|
|
||||||
<Select
|
|
||||||
className="mat-stage-add"
|
|
||||||
filterable
|
|
||||||
options={unusedOptions.map(({ label, value }) => ({ label, value }))}
|
|
||||||
placeholder="+ 添加智能体到本阶段(并行)"
|
|
||||||
value={stageAddValues[stageIndex] ?? undefined}
|
|
||||||
onChange={(value) => {
|
|
||||||
const agent = unusedOptions.find(
|
|
||||||
(option) => option.value === value,
|
|
||||||
)?.agent;
|
|
||||||
if (agent) {
|
|
||||||
addAgentToStage(
|
|
||||||
{ agent_id: agent.id, name: agent.name, description: agent.description ?? undefined },
|
|
||||||
stageIndex,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
setStageAddValues((prev) => ({ ...prev, [stageIndex]: undefined }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<button
|
|
||||||
className="mat-stage-new"
|
|
||||||
onClick={() => updateGroups((groups) => [...groups, { seats: [], goal: "" }])}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Plus size={15} />
|
|
||||||
新增空阶段(串在最后)
|
|
||||||
</button>
|
|
||||||
{!draft.stageGroups.length && (
|
|
||||||
<p className="mat-stage-empty">从左侧候选智能体点击添加,至少编排 {MIN_SEATS} 个智能体。</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 2 && (
|
|
||||||
<div className="mat-editor-pane">
|
|
||||||
<label className="mat-field">
|
|
||||||
<span className="mat-field-label">
|
|
||||||
任务指令<i>*</i>
|
|
||||||
</span>
|
|
||||||
<Textarea
|
|
||||||
autosize={{ minRows: 4, maxRows: 8 }}
|
|
||||||
maxlength={2000}
|
|
||||||
placeholder="描述每次定时执行要让智能体编队完成的事情,例如:汇总过去 24 小时重点舆情并生成早报…"
|
|
||||||
value={draft.prompt}
|
|
||||||
onChange={(value) => updateDraft({ prompt: String(value ?? "") })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="mat-field">
|
|
||||||
<span className="mat-field-label">总控提示(可选)</span>
|
|
||||||
<Textarea
|
|
||||||
autosize={{ minRows: 3, maxRows: 6 }}
|
|
||||||
maxlength={1000}
|
|
||||||
placeholder="写给总控(协调者)的编排要求,例如各阶段如何衔接、产出格式约束等"
|
|
||||||
value={draft.coordinatorPrompt}
|
|
||||||
onChange={(value) => updateDraft({ coordinatorPrompt: String(value ?? "") })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{previewStages.length > 0 && (
|
|
||||||
<div className="mat-field">
|
|
||||||
<span className="mat-field-label">编排预览</span>
|
|
||||||
<div className="mat-editor-preview">
|
|
||||||
<AgentFlow
|
|
||||||
seats={previewSeats}
|
|
||||||
showGoals
|
|
||||||
size="sm"
|
|
||||||
stageGoals={draft.stageGroups.map((group) => group.goal.trim() || null)}
|
|
||||||
stages={previewStages}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<footer className="mat-editor-footer">
|
|
||||||
<span className="mat-editor-hint">
|
|
||||||
{step === 1 ? `已编排 ${seatTotal} 个智能体 · 至少 ${MIN_SEATS} 个` : ""}
|
|
||||||
</span>
|
|
||||||
<div className="mat-editor-footer-ops">
|
|
||||||
<Button variant="outline" onClick={onClose}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
{step > 0 && (
|
|
||||||
<Button variant="outline" onClick={() => setStep((current) => current - 1)}>
|
|
||||||
上一步
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{step < 2 ? (
|
|
||||||
<Button theme="primary" onClick={() => setStep((current) => current + 1)}>
|
|
||||||
下一步
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button loading={submitting} theme="primary" onClick={trySubmit}>
|
|
||||||
{task ? "保存修改" : "创建任务"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,340 @@
|
|||||||
|
/**
|
||||||
|
* 任务分析 · 详情页:左侧执行记录(定时 / 手动),右侧按选中运行回放流程图
|
||||||
|
* (业务链风格节点状态 + 耗时,5 秒自动刷新),点击智能体节点打开执行过程抽屉。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
CalendarClock,
|
||||||
|
CircleAlert,
|
||||||
|
GitBranch,
|
||||||
|
MessageSquareText,
|
||||||
|
Pencil,
|
||||||
|
Pause,
|
||||||
|
Play,
|
||||||
|
RefreshCw,
|
||||||
|
Zap,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { Button, Switch, Tag } from "tdesign-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { OFFICIAL_DEERFLOW_ROUTE_PREFIX } from "@/core/official-deerflow/routes";
|
||||||
|
import {
|
||||||
|
formatDateTime,
|
||||||
|
formatDuration,
|
||||||
|
formatRelative,
|
||||||
|
} from "@/pages/official-multi-agent-tasks/utils";
|
||||||
|
|
||||||
|
import BindingDialog from "./components/BindingDialog";
|
||||||
|
import SeatProcessDrawer from "./components/SeatProcessDrawer";
|
||||||
|
import { ChainFlowDiagram } from "@/roundtable-planning/components/ChainFlowDiagram";
|
||||||
|
import { taskBadgeOf } from "./badge";
|
||||||
|
import { useTaTaskActions, useTaTasks, useTaTaskRuns } from "./store";
|
||||||
|
|
||||||
|
const RUN_STATUS_TEXT: Record<string, string> = {
|
||||||
|
succeeded: "成功",
|
||||||
|
failed: "失败",
|
||||||
|
running: "执行中",
|
||||||
|
pending: "排队中",
|
||||||
|
};
|
||||||
|
|
||||||
|
const RUN_STATUS_THEME: Record<string, "success" | "danger" | "warning" | "default"> = {
|
||||||
|
succeeded: "success",
|
||||||
|
failed: "danger",
|
||||||
|
running: "warning",
|
||||||
|
pending: "default",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TaskAnalysisDetailPage({
|
||||||
|
routeTaskId,
|
||||||
|
}: {
|
||||||
|
routeTaskId: string | null;
|
||||||
|
}) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { views, isLoading } = useTaTasks();
|
||||||
|
const actions = useTaTaskActions();
|
||||||
|
const taskView = views.find(({ task }) => task.id === routeTaskId) ?? null;
|
||||||
|
const task = taskView?.task ?? null;
|
||||||
|
const { runs, isLoading: runsLoading } = useTaTaskRuns(task?.id ?? null);
|
||||||
|
|
||||||
|
const [selectedRunId, setSelectedRunId] = useState<string | null>(null);
|
||||||
|
const [seatAgentId, setSeatAgentId] = useState<string | null>(null);
|
||||||
|
const [bindingVisible, setBindingVisible] = useState(false);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
|
// 默认选中最近一次运行;新触发的运行自动跟随。
|
||||||
|
useEffect(() => {
|
||||||
|
if (!runs.length) return;
|
||||||
|
if (!selectedRunId || !runs.some((run) => run.id === selectedRunId)) {
|
||||||
|
setSelectedRunId(runs[0]!.id);
|
||||||
|
}
|
||||||
|
}, [runs, selectedRunId]);
|
||||||
|
|
||||||
|
const selectedRun = useMemo(
|
||||||
|
() => runs.find((run) => run.id === selectedRunId) ?? runs[0] ?? null,
|
||||||
|
[runs, selectedRunId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const backToList = () => {
|
||||||
|
navigate(`${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/task-analysis`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const runTrigger = async (manual: boolean) => {
|
||||||
|
if (!task) return;
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
if (manual) {
|
||||||
|
await actions.triggerManualRun.mutateAsync(task.id);
|
||||||
|
toast.success("已发起手动执行(总控问答)");
|
||||||
|
} else {
|
||||||
|
await actions.triggerRun.mutateAsync(task.id);
|
||||||
|
toast.success("已触发定时执行");
|
||||||
|
}
|
||||||
|
setSelectedRunId(null);
|
||||||
|
} catch {
|
||||||
|
toast.error("任务未绑定分析方案");
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const runToggle = async () => {
|
||||||
|
if (!task?.binding) return;
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await actions.setEnabled.mutateAsync({
|
||||||
|
taskId: task.id,
|
||||||
|
enabled: !task.binding?.enabled,
|
||||||
|
});
|
||||||
|
toast.success(task.binding?.enabled ? "已暂停" : "已恢复");
|
||||||
|
} catch {
|
||||||
|
toast.error("操作失败,请重试");
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isLoading && !task) {
|
||||||
|
return (
|
||||||
|
<section className="ta-page">
|
||||||
|
<div className="ta-state ta-state--empty">
|
||||||
|
<h3>任务不存在或已被删除</h3>
|
||||||
|
<Button theme="primary" onClick={backToList}>
|
||||||
|
返回任务列表
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const binding = task?.binding ?? null;
|
||||||
|
const taskBadge = task && taskView ? taskBadgeOf(task, taskView.runs) : null;
|
||||||
|
const live = selectedRun?.status === "running" || selectedRun?.status === "pending";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="ta-page ta-detail-page">
|
||||||
|
<header className="ta-detail-head">
|
||||||
|
<button
|
||||||
|
aria-label="返回任务列表"
|
||||||
|
className="ta-back"
|
||||||
|
onClick={backToList}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<ArrowLeft size={18} />
|
||||||
|
</button>
|
||||||
|
<div className="ta-detail-heading">
|
||||||
|
<h1>
|
||||||
|
{task?.name ?? "…"}
|
||||||
|
<em>{task?.sourceTaskId}</em>
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
{binding ? (
|
||||||
|
<>
|
||||||
|
<GitBranch aria-hidden="true" size={13} />
|
||||||
|
{binding.chainTitle} · {binding.cronText}
|
||||||
|
{binding.directionNotes.length > 0 &&
|
||||||
|
` · ${binding.directionNotes.length} 条方向备注`}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
task?.description
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{taskBadge && (
|
||||||
|
<Tag theme={taskBadge.theme} variant="light">
|
||||||
|
{taskBadge.label}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{binding && (
|
||||||
|
<Switch value={binding.enabled} onChange={() => void runToggle()} />
|
||||||
|
)}
|
||||||
|
<div className="ta-detail-ops">
|
||||||
|
{binding && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
disabled={busy}
|
||||||
|
loading={busy}
|
||||||
|
theme="primary"
|
||||||
|
onClick={() => void runTrigger(false)}
|
||||||
|
>
|
||||||
|
<Zap aria-hidden="true" size={15} />
|
||||||
|
立即执行
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" disabled={busy} onClick={() => void runTrigger(true)}>
|
||||||
|
<MessageSquareText aria-hidden="true" size={15} />
|
||||||
|
模拟手动执行
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Button variant="outline" onClick={() => setBindingVisible(true)}>
|
||||||
|
<Pencil aria-hidden="true" size={15} />
|
||||||
|
{binding ? "编辑绑定" : "配置分析"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{!binding ? (
|
||||||
|
<div className="ta-detail-empty">
|
||||||
|
<GitBranch aria-hidden="true" size={26} />
|
||||||
|
<h3>尚未配置分析方案</h3>
|
||||||
|
<p>绑定业务链与内容块映射后,这里将展示执行记录与各智能体的执行流程。</p>
|
||||||
|
<Button theme="primary" onClick={() => setBindingVisible(true)}>
|
||||||
|
配置分析
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="ta-detail-layout">
|
||||||
|
<aside className="ta-runs">
|
||||||
|
<header className="ta-runs-head">
|
||||||
|
<h2>执行记录</h2>
|
||||||
|
<span>
|
||||||
|
<RefreshCw aria-hidden="true" className={live ? "ta-spin" : undefined} size={12} />
|
||||||
|
每 5 秒刷新
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<div className="ta-runs-list">
|
||||||
|
{runsLoading && !runs.length ? (
|
||||||
|
<p className="ta-runs-empty">正在加载执行记录…</p>
|
||||||
|
) : runs.length ? (
|
||||||
|
runs.map((run) => {
|
||||||
|
const active = selectedRun?.id === run.id;
|
||||||
|
const duration = run.finishedAt
|
||||||
|
? formatDuration(
|
||||||
|
new Date(run.finishedAt).getTime() - new Date(run.startedAt).getTime(),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`ta-run-item${active ? " is-active" : ""}`}
|
||||||
|
key={run.id}
|
||||||
|
onClick={() => setSelectedRunId(run.id)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className="ta-run-item-top">
|
||||||
|
{run.trigger === "manual" ? (
|
||||||
|
<MessageSquareText aria-hidden="true" size={13} />
|
||||||
|
) : (
|
||||||
|
<CalendarClock aria-hidden="true" size={13} />
|
||||||
|
)}
|
||||||
|
<b>{run.trigger === "manual" ? "手动执行" : "定时执行"}</b>
|
||||||
|
<Tag size="small" theme={RUN_STATUS_THEME[run.status] ?? "default"} variant="light">
|
||||||
|
{RUN_STATUS_TEXT[run.status] ?? run.status}
|
||||||
|
</Tag>
|
||||||
|
</span>
|
||||||
|
<span className="ta-run-item-time">
|
||||||
|
{formatDateTime(run.startedAt)} · {duration ?? "进行中"}
|
||||||
|
</span>
|
||||||
|
{run.trigger === "manual" && run.manualSummary && (
|
||||||
|
<span className="ta-run-item-summary">{run.manualSummary}</span>
|
||||||
|
)}
|
||||||
|
{run.trigger === "schedule" && (
|
||||||
|
<span className="ta-run-item-blocks">
|
||||||
|
内容块 {run.blockStates.filter((b) => b.state === "done").length}/
|
||||||
|
{run.blockStates.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<p className="ta-runs-empty">还没有执行记录,点「立即执行」体验完整流程。</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section className="ta-flow-panel">
|
||||||
|
{selectedRun ? (
|
||||||
|
<>
|
||||||
|
<header className="ta-flow-head">
|
||||||
|
<h2>
|
||||||
|
执行流程
|
||||||
|
<small>
|
||||||
|
{selectedRun.trigger === "manual" ? "手动执行" : "定时执行"} ·{" "}
|
||||||
|
{formatRelative(selectedRun.startedAt)}
|
||||||
|
{selectedRun.status === "running" &&
|
||||||
|
` · 阶段 ${Math.min(selectedRun.stageIndex + 1, selectedRun.stageTotal)}/${selectedRun.stageTotal} 执行中`}
|
||||||
|
</small>
|
||||||
|
</h2>
|
||||||
|
<span className="ta-flow-legend">
|
||||||
|
<i className="ta-dot is-done" /> 完成
|
||||||
|
<i className="ta-dot is-running" /> 执行中
|
||||||
|
<i className="ta-dot is-waiting" /> 等待
|
||||||
|
<i className="ta-dot is-failed" /> 失败
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<div className="ta-flow-canvas">
|
||||||
|
<ChainFlowDiagram
|
||||||
|
className="ta-flow-diagram"
|
||||||
|
onNodeClick={(agentId) => setSeatAgentId(agentId)}
|
||||||
|
seatStates={selectedRun.seatInfos}
|
||||||
|
stages={selectedRun.stages.map((stage, index) => ({
|
||||||
|
seats: stage.map((agentId) => ({
|
||||||
|
agent_id: agentId,
|
||||||
|
name: selectedRun.seatNames[agentId] ?? agentId,
|
||||||
|
})),
|
||||||
|
goal: selectedRun.stageGoals?.[index] ?? null,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{selectedRun.error && (
|
||||||
|
<p className="ta-flow-error">
|
||||||
|
<CircleAlert aria-hidden="true" size={14} />
|
||||||
|
{selectedRun.error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="ta-flow-panel-empty">选择左侧一条执行记录查看流程</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SeatProcessDrawer
|
||||||
|
onClose={() => setSeatAgentId(null)}
|
||||||
|
run={selectedRun}
|
||||||
|
seatAgentId={seatAgentId}
|
||||||
|
task={task ?? undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BindingDialog
|
||||||
|
onClose={() => setBindingVisible(false)}
|
||||||
|
onSubmit={async (payload) => {
|
||||||
|
if (!task) return;
|
||||||
|
try {
|
||||||
|
await actions.bind.mutateAsync({ taskId: task.id, payload });
|
||||||
|
toast.success(binding ? "绑定已更新" : "分析方案已绑定");
|
||||||
|
setBindingVisible(false);
|
||||||
|
} catch {
|
||||||
|
toast.error("保存失败,请重试");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
submitting={actions.bind.isPending}
|
||||||
|
task={task}
|
||||||
|
visible={bindingVisible}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,460 @@
|
|||||||
|
/**
|
||||||
|
* 任务分析 · 列表页:对方系统推送过来的外部任务 + 我们的绑定状态。
|
||||||
|
* 任务卡片沿用多智能体定时任务的卡片视觉(浅红规范)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
CalendarClock,
|
||||||
|
CircleAlert,
|
||||||
|
CircleCheck,
|
||||||
|
Clock3,
|
||||||
|
GitBranch,
|
||||||
|
Link2,
|
||||||
|
Network,
|
||||||
|
Pencil,
|
||||||
|
Pause,
|
||||||
|
Play,
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
Trash2,
|
||||||
|
Zap,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DialogPlugin,
|
||||||
|
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 "@/pages/official-multi-agent-tasks/components/AgentFlow";
|
||||||
|
import type { MatSeat } from "@/pages/official-multi-agent-tasks/types";
|
||||||
|
import {
|
||||||
|
formatDateTime,
|
||||||
|
formatRelative,
|
||||||
|
} from "@/pages/official-multi-agent-tasks/utils";
|
||||||
|
|
||||||
|
import BindingDialog from "./components/BindingDialog";
|
||||||
|
import CreateTaskDialog, {
|
||||||
|
type TaCreateTaskPayload,
|
||||||
|
} from "./components/CreateTaskDialog";
|
||||||
|
import { chainStagesOf, useTaTaskActions, useTaTasks } from "./store";
|
||||||
|
import type { TaTask, TaTaskViewLike } from "./page-types";
|
||||||
|
import { taskBadgeOf } from "./badge";
|
||||||
|
|
||||||
|
import "./task-analysis.css";
|
||||||
|
|
||||||
|
type StatusFilter = "all" | "unbound" | "enabled" | "paused" | "failed";
|
||||||
|
|
||||||
|
function isTaskLive(runs: TaTaskViewLike["runs"]) {
|
||||||
|
return runs.some((run) => run.status === "running" || run.status === "pending");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TaskAnalysisPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { views, isLoading } = useTaTasks();
|
||||||
|
const actions = useTaTaskActions();
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [filter, setFilter] = useState<StatusFilter>("all");
|
||||||
|
const [bindingTask, setBindingTask] = useState<TaTask | null>(null);
|
||||||
|
const [bindingVisible, setBindingVisible] = useState(false);
|
||||||
|
const [createVisible, setCreateVisible] = useState(false);
|
||||||
|
const [busyTaskId, setBusyTaskId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const visibleViews = useMemo(() => {
|
||||||
|
const keyword = search.trim().toLowerCase();
|
||||||
|
return views.filter(({ task, runs }) => {
|
||||||
|
const lastStatus = runs[0]?.status ?? null;
|
||||||
|
const bound = !!task.binding;
|
||||||
|
const live = isTaskLive(runs);
|
||||||
|
if (filter === "unbound" && bound) return false;
|
||||||
|
if (filter === "enabled" && (!bound || !task.binding!.enabled || live)) return false;
|
||||||
|
if (filter === "paused" && (!bound || task.binding!.enabled)) return false;
|
||||||
|
if (filter === "failed" && lastStatus !== "failed") return false;
|
||||||
|
if (!keyword) return true;
|
||||||
|
return (
|
||||||
|
task.name.toLowerCase().includes(keyword) ||
|
||||||
|
task.description.toLowerCase().includes(keyword) ||
|
||||||
|
task.sourceTaskId.toLowerCase().includes(keyword)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [views, search, filter]);
|
||||||
|
|
||||||
|
const stats = useMemo(
|
||||||
|
() => ({
|
||||||
|
total: views.length,
|
||||||
|
unbound: views.filter(({ task }) => !task.binding).length,
|
||||||
|
enabled: views.filter(({ task }) => task.binding?.enabled).length,
|
||||||
|
failed: views.filter(({ runs }) => runs[0]?.status === "failed").length,
|
||||||
|
}),
|
||||||
|
[views],
|
||||||
|
);
|
||||||
|
|
||||||
|
const openDetail = (taskId: string) => {
|
||||||
|
navigate(`${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/task-analysis/${encodeURIComponent(taskId)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openBinding = (task: TaTask) => {
|
||||||
|
setBindingTask(task);
|
||||||
|
setBindingVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = async (payload: TaCreateTaskPayload) => {
|
||||||
|
try {
|
||||||
|
const created = await actions.createTask.mutateAsync({
|
||||||
|
name: payload.name,
|
||||||
|
sourceTaskId: payload.sourceTaskId,
|
||||||
|
description: payload.description,
|
||||||
|
});
|
||||||
|
await actions.bind.mutateAsync({ taskId: created.id, payload: payload.binding });
|
||||||
|
toast.success("任务已创建并绑定分析方案");
|
||||||
|
setCreateVisible(false);
|
||||||
|
} catch {
|
||||||
|
toast.error("创建失败,请重试");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBind = async (payload: Parameters<typeof actions.bind.mutateAsync>[0]["payload"]) => {
|
||||||
|
if (!bindingTask) return;
|
||||||
|
try {
|
||||||
|
await actions.bind.mutateAsync({ taskId: bindingTask.id, payload });
|
||||||
|
toast.success(bindingTask.binding ? "绑定已更新" : "分析方案已绑定");
|
||||||
|
setBindingVisible(false);
|
||||||
|
setBindingTask(null);
|
||||||
|
} catch {
|
||||||
|
toast.error("保存失败,请重试");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const runTrigger = async (taskId: string) => {
|
||||||
|
setBusyTaskId(taskId);
|
||||||
|
try {
|
||||||
|
await actions.triggerRun.mutateAsync(taskId);
|
||||||
|
toast.success("已触发执行");
|
||||||
|
openDetail(taskId);
|
||||||
|
} catch {
|
||||||
|
toast.error("触发失败:任务未绑定分析方案");
|
||||||
|
} finally {
|
||||||
|
setBusyTaskId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const runToggle = async (task: TaTask) => {
|
||||||
|
setBusyTaskId(task.id);
|
||||||
|
try {
|
||||||
|
await actions.setEnabled.mutateAsync({ taskId: task.id, enabled: !task.binding?.enabled });
|
||||||
|
toast.success(task.binding?.enabled ? "已暂停" : "已恢复");
|
||||||
|
} catch {
|
||||||
|
toast.error("操作失败,请重试");
|
||||||
|
} finally {
|
||||||
|
setBusyTaskId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDelete = (task: TaTask) => {
|
||||||
|
const dialog = DialogPlugin.confirm({
|
||||||
|
header: "删除外部任务",
|
||||||
|
body: `删除「${task.name}」将同时清除其绑定与全部执行记录(对应对外删除接口语义),确定删除?`,
|
||||||
|
confirmBtn: { content: "删除", theme: "danger" },
|
||||||
|
cancelBtn: "取消",
|
||||||
|
onConfirm: () => {
|
||||||
|
dialog.destroy();
|
||||||
|
void actions.removeTask.mutateAsync(task.id).then(() => {
|
||||||
|
toast.success("任务已删除");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onClose: () => dialog.destroy(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="ta-page">
|
||||||
|
<header className="ta-page-head">
|
||||||
|
<div className="ta-page-head-copy">
|
||||||
|
<h1>任务分析定时</h1>
|
||||||
|
<p>
|
||||||
|
外部系统任务接入多智能体定时分析:绑定业务链与内容块映射,按计划执行并沉淀时间线版本。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="ta-page-head-actions">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const unbound = views.find(({ task }) => !task.binding)?.task;
|
||||||
|
if (unbound) openBinding(unbound);
|
||||||
|
else toast.info("当前任务均已绑定分析方案");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
配置未绑定任务
|
||||||
|
</Button>
|
||||||
|
<Button theme="primary" onClick={() => setCreateVisible(true)}>
|
||||||
|
<Plus aria-hidden="true" size={15} />
|
||||||
|
新建任务
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="ta-stats" aria-label="任务统计">
|
||||||
|
<div className="ta-stat">
|
||||||
|
<span className="ta-stat-icon is-primary" aria-hidden="true">
|
||||||
|
<Network size={18} />
|
||||||
|
</span>
|
||||||
|
<div className="ta-stat-copy">
|
||||||
|
<strong>{stats.total}</strong>
|
||||||
|
<span>外部任务</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ta-stat">
|
||||||
|
<span className="ta-stat-icon is-warning" aria-hidden="true">
|
||||||
|
<Link2 size={18} />
|
||||||
|
</span>
|
||||||
|
<div className="ta-stat-copy">
|
||||||
|
<strong>{stats.unbound}</strong>
|
||||||
|
<span>未配置</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ta-stat">
|
||||||
|
<span className="ta-stat-icon is-success" aria-hidden="true">
|
||||||
|
<CircleCheck size={18} />
|
||||||
|
</span>
|
||||||
|
<div className="ta-stat-copy">
|
||||||
|
<strong>{stats.enabled}</strong>
|
||||||
|
<span>已启用</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ta-stat">
|
||||||
|
<span className="ta-stat-icon is-danger" aria-hidden="true">
|
||||||
|
<CircleAlert size={18} />
|
||||||
|
</span>
|
||||||
|
<div className="ta-stat-copy">
|
||||||
|
<strong>{stats.failed}</strong>
|
||||||
|
<span>最近失败</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ta-body">
|
||||||
|
<div className="ta-toolbar">
|
||||||
|
<div className="ta-toolbar-search">
|
||||||
|
<Input
|
||||||
|
clearable
|
||||||
|
placeholder="搜索任务名称 / 简介 / 对方任务号"
|
||||||
|
prefixIcon={<Search aria-hidden="true" size={15} />}
|
||||||
|
value={search}
|
||||||
|
onChange={(value) => setSearch(String(value ?? ""))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Tabs
|
||||||
|
className="ta-toolbar-tabs"
|
||||||
|
value={filter}
|
||||||
|
onChange={(value) => setFilter(value as StatusFilter)}
|
||||||
|
>
|
||||||
|
<Tabs.TabPanel label="全部" value="all" />
|
||||||
|
<Tabs.TabPanel label="未配置" value="unbound" />
|
||||||
|
<Tabs.TabPanel label="已启用" value="enabled" />
|
||||||
|
<Tabs.TabPanel label="已暂停" value="paused" />
|
||||||
|
<Tabs.TabPanel label="最近失败" value="failed" />
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="ta-state">
|
||||||
|
<Clock3 aria-hidden="true" className="ta-spin" size={18} />
|
||||||
|
正在加载外部任务…
|
||||||
|
</div>
|
||||||
|
) : visibleViews.length ? (
|
||||||
|
<div className="ta-grid">
|
||||||
|
{visibleViews.map(({ task, runs }) => {
|
||||||
|
const badge = taskBadgeOf(task, runs);
|
||||||
|
const binding = task.binding;
|
||||||
|
const lastRun = runs[0] ?? null;
|
||||||
|
const busy = busyTaskId === task.id;
|
||||||
|
const stages = binding ? chainStagesOf(binding) : [];
|
||||||
|
const seats: MatSeat[] = stages
|
||||||
|
.flat()
|
||||||
|
.filter((id, index, all) => all.indexOf(id) === index)
|
||||||
|
.map((id) => ({
|
||||||
|
agent_id: id,
|
||||||
|
name: lastRun?.seatNames[id] ?? id,
|
||||||
|
}));
|
||||||
|
return (
|
||||||
|
<article className="ta-card" key={task.id}>
|
||||||
|
<header className="ta-card-head" onClick={() => openDetail(task.id)}>
|
||||||
|
<span className="ta-card-glyph" aria-hidden="true">
|
||||||
|
<Network size={17} />
|
||||||
|
</span>
|
||||||
|
<div className="ta-card-title">
|
||||||
|
<h3>
|
||||||
|
{task.name}
|
||||||
|
<em>{task.sourceTaskId}</em>
|
||||||
|
</h3>
|
||||||
|
<p>{task.description}</p>
|
||||||
|
</div>
|
||||||
|
<Tag size="small" theme={badge.theme} variant="light">
|
||||||
|
{badge.label}
|
||||||
|
</Tag>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{binding ? (
|
||||||
|
<>
|
||||||
|
<div className="ta-card-binding" onClick={() => openDetail(task.id)}>
|
||||||
|
<span>
|
||||||
|
<GitBranch aria-hidden="true" size={13} />
|
||||||
|
{binding.chainTitle}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<CalendarClock aria-hidden="true" size={13} />
|
||||||
|
{binding.cronText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="ta-card-flow" onClick={() => openDetail(task.id)}>
|
||||||
|
<AgentFlow seats={seats} size="sm" stages={stages} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button className="ta-card-unbound" onClick={() => openBinding(task)} type="button">
|
||||||
|
<GitBranch aria-hidden="true" size={15} />
|
||||||
|
配置分析:选择业务链 · 内容块映射 · 执行频率
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<footer className="ta-card-foot">
|
||||||
|
<div className="ta-card-meta">
|
||||||
|
<span>
|
||||||
|
下次{" "}
|
||||||
|
{binding?.enabled
|
||||||
|
? formatDateTime(nextRunAt(binding.cron))
|
||||||
|
: binding
|
||||||
|
? "已暂停"
|
||||||
|
: "未配置"}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
上次 {lastRun ? formatRelative(lastRun.startedAt) : "未执行"}
|
||||||
|
{lastRun && lastRun.status !== "pending" && (
|
||||||
|
<em className={`ta-run-text is-${lastRun.status}`}>
|
||||||
|
· {lastRun.status === "succeeded" ? "成功" : lastRun.status === "failed" ? "失败" : "执行中"}
|
||||||
|
</em>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="ta-card-ops">
|
||||||
|
{binding && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
disabled={busy}
|
||||||
|
loading={busy}
|
||||||
|
size="small"
|
||||||
|
theme="primary"
|
||||||
|
variant="text"
|
||||||
|
onClick={() => void runTrigger(task.id)}
|
||||||
|
>
|
||||||
|
<Zap aria-hidden="true" size={14} />
|
||||||
|
立即执行
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={busy}
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
onClick={() => void runToggle(task)}
|
||||||
|
>
|
||||||
|
{binding.enabled ? (
|
||||||
|
<Pause aria-hidden="true" size={14} />
|
||||||
|
) : (
|
||||||
|
<Play aria-hidden="true" size={14} />
|
||||||
|
)}
|
||||||
|
{binding.enabled ? "暂停" : "恢复"}
|
||||||
|
</Button>
|
||||||
|
<Button size="small" variant="text" onClick={() => openBinding(task)}>
|
||||||
|
<Pencil aria-hidden="true" size={14} />
|
||||||
|
编辑绑定
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Popconfirm
|
||||||
|
content="删除后绑定与执行记录一并清除,确定?"
|
||||||
|
placement="bottom-right"
|
||||||
|
theme="danger"
|
||||||
|
onConfirm={() => confirmDelete(task)}
|
||||||
|
>
|
||||||
|
<Button className="ta-danger-btn" size="small" variant="text">
|
||||||
|
<Trash2 aria-hidden="true" size={14} />
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="ta-state ta-state--empty">
|
||||||
|
<span className="ta-state-glyph" aria-hidden="true">
|
||||||
|
<Network size={22} />
|
||||||
|
</span>
|
||||||
|
<h3>{views.length ? "没有符合条件的任务" : "还没有外部任务"}</h3>
|
||||||
|
<p>
|
||||||
|
{views.length
|
||||||
|
? "换个关键词或筛选条件试试。"
|
||||||
|
: "对方系统创建任务并调用创建接口后,任务会出现在这里。"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="ta-footnote">
|
||||||
|
<CircleAlert aria-hidden="true" size={13} />
|
||||||
|
前端演示阶段:任务与执行数据保存在本浏览器,业务链来自真实接口;后端服务接入后自动切换。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CreateTaskDialog
|
||||||
|
onClose={() => setCreateVisible(false)}
|
||||||
|
onSubmit={handleCreate}
|
||||||
|
submitting={actions.createTask.isPending}
|
||||||
|
visible={createVisible}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BindingDialog
|
||||||
|
onClose={() => {
|
||||||
|
setBindingVisible(false);
|
||||||
|
setBindingTask(null);
|
||||||
|
}}
|
||||||
|
onSubmit={handleBind}
|
||||||
|
submitting={actions.bind.isPending}
|
||||||
|
task={bindingTask}
|
||||||
|
visible={bindingVisible}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 简易下次执行时间(覆盖本页生成的 cron 形态)。 */
|
||||||
|
function nextRunAt(cron: string): string | null {
|
||||||
|
const parts = cron.trim().split(/\s+/);
|
||||||
|
if (parts.length !== 5) return null;
|
||||||
|
const [min, hour, , , wd] = parts;
|
||||||
|
const now = new Date();
|
||||||
|
const candidate = new Date(now);
|
||||||
|
const minute = parseInt(min ?? "0", 10);
|
||||||
|
const hourNum = parseInt(hour ?? "9", 10);
|
||||||
|
if (`${hour}` === "*") {
|
||||||
|
candidate.setMinutes(minute, 0, 0);
|
||||||
|
if (candidate <= now) candidate.setHours(candidate.getHours() + 1);
|
||||||
|
} else if (`${wd}` !== "*") {
|
||||||
|
const weekday = parseInt(wd ?? "1", 10);
|
||||||
|
candidate.setHours(hourNum, minute, 0, 0);
|
||||||
|
let delta = (weekday - candidate.getDay() + 7) % 7;
|
||||||
|
if (delta === 0 && candidate <= now) delta = 7;
|
||||||
|
candidate.setDate(candidate.getDate() + delta);
|
||||||
|
} else {
|
||||||
|
candidate.setHours(hourNum, minute, 0, 0);
|
||||||
|
if (candidate <= now) candidate.setDate(candidate.getDate() + 1);
|
||||||
|
}
|
||||||
|
return candidate.toISOString();
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { TaRunView, TaTask } from "./types";
|
||||||
|
|
||||||
|
export interface TaBadge {
|
||||||
|
label: string;
|
||||||
|
theme: "success" | "warning" | "danger" | "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 任务卡右上角状态徽标:最近一次执行优先,其次启停,未绑定给引导态。 */
|
||||||
|
export function taskBadgeOf(task: TaTask, runs: TaRunView[]): TaBadge {
|
||||||
|
const status = runs[0]?.status;
|
||||||
|
if (status === "running" || status === "pending") {
|
||||||
|
return { label: "执行中", theme: "warning" };
|
||||||
|
}
|
||||||
|
if (!task.binding) return { label: "未配置", theme: "default" };
|
||||||
|
if (!task.binding.enabled) return { label: "已暂停", theme: "default" };
|
||||||
|
if (status === "failed") return { label: "上次失败", theme: "danger" };
|
||||||
|
if (status === "succeeded") return { label: "运行正常", theme: "success" };
|
||||||
|
return { label: "已启用", theme: "success" };
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
/**
|
||||||
|
* 绑定配置弹窗:选业务链(真实 /api/roundtable-chains + 内置演示链)→ 配置
|
||||||
|
* 对方详情块 → 席位映射 → 执行频率 / 启用 / 总控提示。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Radio } from "lucide-react";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { Button, Dialog, Switch, Textarea } from "tdesign-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { listChains } from "@/roundtable-planning/api/chains";
|
||||||
|
import type { BusinessChain } from "@/roundtable-planning/lib/business-chain";
|
||||||
|
import { type ScheduleConfig } from "@/components/workspace/scheduled-tasks/schedule-builder";
|
||||||
|
|
||||||
|
import AgentFlow from "@/pages/official-multi-agent-tasks/components/AgentFlow";
|
||||||
|
import SchedulePicker from "@/pages/official-multi-agent-tasks/components/SchedulePicker";
|
||||||
|
import type { MatSeat } from "@/pages/official-multi-agent-tasks/types";
|
||||||
|
import {
|
||||||
|
defaultScheduleConfig,
|
||||||
|
describeScheduleConfig,
|
||||||
|
} from "@/pages/official-multi-agent-tasks/utils";
|
||||||
|
|
||||||
|
import type { TaBinding, TaBindingPayload, TaTask } from "../types";
|
||||||
|
|
||||||
|
/** 内置演示链(种子里用它,保证无后端数据时链路可演示)。 */
|
||||||
|
export const SEED_CHAIN: BusinessChain = {
|
||||||
|
id: "seed-chain-std",
|
||||||
|
title: "标准舆情分析链(内置)",
|
||||||
|
description: "采集 + 核查并行 → 研判 → 成稿,演示用内置编排。",
|
||||||
|
seats: [
|
||||||
|
{ agent_id: "collector", name: "全网舆情采集" },
|
||||||
|
{ agent_id: "verifier", name: "数据核查员" },
|
||||||
|
{ agent_id: "analyst", name: "舆情研判分析" },
|
||||||
|
{ agent_id: "writer", name: "报告撰写助手" },
|
||||||
|
],
|
||||||
|
stages: [
|
||||||
|
["collector", "verifier"],
|
||||||
|
["analyst"],
|
||||||
|
["writer"],
|
||||||
|
],
|
||||||
|
stageGoals: null,
|
||||||
|
isPublic: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function chainStages(chain: BusinessChain): string[][] {
|
||||||
|
if (chain.stages?.length) return chain.stages;
|
||||||
|
return chain.seats.map((seat) => [seat.agent_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function chainSeats(chain: BusinessChain): MatSeat[] {
|
||||||
|
return chain.seats.map((seat) => ({
|
||||||
|
agent_id: seat.agent_id,
|
||||||
|
name: seat.name,
|
||||||
|
description: seat.description,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 模块级空数组:data 未就绪时保持引用稳定,避免 effect 依赖无限循环。 */
|
||||||
|
export const NO_CHAINS: BusinessChain[] = [];
|
||||||
|
|
||||||
|
export default function BindingDialog({
|
||||||
|
visible,
|
||||||
|
task,
|
||||||
|
submitting,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
visible: boolean;
|
||||||
|
task: TaTask | null;
|
||||||
|
submitting: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (payload: TaBindingPayload) => void;
|
||||||
|
}) {
|
||||||
|
const [chainId, setChainId] = useState("");
|
||||||
|
const [schedule, setSchedule] = useState<ScheduleConfig>(defaultScheduleConfig());
|
||||||
|
const [enabled, setEnabled] = useState(true);
|
||||||
|
const [coordinatorPrompt, setCoordinatorPrompt] = useState("");
|
||||||
|
|
||||||
|
const { data, isLoading: chainsLoading } = useQuery({
|
||||||
|
queryKey: ["ta", "chains"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const [mine, publicChains] = await Promise.all([
|
||||||
|
listChains("mine").catch(() => [] as BusinessChain[]),
|
||||||
|
listChains("public").catch(() => [] as BusinessChain[]),
|
||||||
|
]);
|
||||||
|
const byId = new Map<string, BusinessChain>();
|
||||||
|
for (const chain of [SEED_CHAIN, ...mine, ...publicChains]) {
|
||||||
|
if (!byId.has(chain.id)) byId.set(chain.id, chain);
|
||||||
|
}
|
||||||
|
return [...byId.values()];
|
||||||
|
},
|
||||||
|
enabled: visible,
|
||||||
|
});
|
||||||
|
const chains = data ?? NO_CHAINS;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) return;
|
||||||
|
const binding: TaBinding | null = task?.binding ?? null;
|
||||||
|
const initialChain =
|
||||||
|
chains.find((chain) => chain.id === binding?.chainId) ??
|
||||||
|
chains[0] ??
|
||||||
|
SEED_CHAIN;
|
||||||
|
setChainId(binding?.chainId ?? initialChain.id);
|
||||||
|
setEnabled(binding?.enabled ?? true);
|
||||||
|
setCoordinatorPrompt(binding?.coordinatorPrompt ?? "");
|
||||||
|
}, [visible, task, chains]);
|
||||||
|
|
||||||
|
const selectedChain = useMemo(
|
||||||
|
() => chains.find((chain) => chain.id === chainId) ?? null,
|
||||||
|
[chains, chainId],
|
||||||
|
);
|
||||||
|
const handleChainChange = (nextId: string) => {
|
||||||
|
setChainId(nextId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const trySubmit = () => {
|
||||||
|
if (!selectedChain) {
|
||||||
|
toast.warning("请选择业务链");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSubmit({
|
||||||
|
chainId: selectedChain.id,
|
||||||
|
chainTitle: selectedChain.title,
|
||||||
|
schedule,
|
||||||
|
enabled,
|
||||||
|
coordinatorPrompt: coordinatorPrompt.trim() || undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
className="ta-binding-dialog"
|
||||||
|
closeBtn
|
||||||
|
closeOnOverlayClick={false}
|
||||||
|
destroyOnClose
|
||||||
|
footer={
|
||||||
|
<div className="ta-dialog-footer">
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button loading={submitting} theme="primary" onClick={trySubmit}>
|
||||||
|
{task?.binding ? "保存绑定" : "创建绑定"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
header={task?.binding ? `编辑绑定 · ${task.name}` : `配置分析 · ${task?.name ?? ""}`}
|
||||||
|
placement="top"
|
||||||
|
visible={visible}
|
||||||
|
width={860}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<div className="ta-binding">
|
||||||
|
<section className="ta-binding-section">
|
||||||
|
<header className="ta-binding-section-head">
|
||||||
|
<h4>选择业务链</h4>
|
||||||
|
<span>定时执行将按业务链的多智能体关系(同阶段并行 · 阶段间串行)运行</span>
|
||||||
|
</header>
|
||||||
|
{chainsLoading ? (
|
||||||
|
<p className="ta-binding-tip">正在加载业务链…</p>
|
||||||
|
) : (
|
||||||
|
<div className="ta-chain-grid">
|
||||||
|
{chains.map((chain) => {
|
||||||
|
const active = chain.id === chainId;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`ta-chain-card${active ? " is-active" : ""}`}
|
||||||
|
key={chain.id}
|
||||||
|
onClick={() => handleChainChange(chain.id)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<Radio aria-hidden="true" size={14} />
|
||||||
|
<span className="ta-chain-card-title">{chain.title}</span>
|
||||||
|
<em>
|
||||||
|
{chain.seats.length} 智能体 · {chainStages(chain).length} 阶段
|
||||||
|
</em>
|
||||||
|
</header>
|
||||||
|
{chain.description && <p>{chain.description}</p>}
|
||||||
|
<div className="ta-chain-card-flow">
|
||||||
|
<AgentFlow
|
||||||
|
seats={chainSeats(chain)}
|
||||||
|
size="sm"
|
||||||
|
stages={chainStages(chain)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="ta-binding-section">
|
||||||
|
<header className="ta-binding-section-head">
|
||||||
|
<h4>执行设置</h4>
|
||||||
|
<span>{describeScheduleConfig(schedule)} 自动执行</span>
|
||||||
|
</header>
|
||||||
|
<div className="ta-binding-settings">
|
||||||
|
<div className="ta-field">
|
||||||
|
<span className="ta-field-label">执行频率</span>
|
||||||
|
<SchedulePicker value={schedule} onChange={setSchedule} />
|
||||||
|
</div>
|
||||||
|
<label className="ta-field">
|
||||||
|
<span className="ta-field-label">总控提示(可选)</span>
|
||||||
|
<Textarea
|
||||||
|
autosize={{ minRows: 2, maxRows: 4 }}
|
||||||
|
maxlength={500}
|
||||||
|
placeholder="写给总控(协调者)的编排要求;问答沉淀的「方向备注」也会注入后续执行"
|
||||||
|
value={coordinatorPrompt}
|
||||||
|
onChange={(value) => setCoordinatorPrompt(String(value ?? ""))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="ta-field ta-field--switch">
|
||||||
|
<span className="ta-field-label">绑定后立即启用</span>
|
||||||
|
<Switch value={enabled} onChange={(value) => setEnabled(!!value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
/**
|
||||||
|
* 新建任务弹窗(一步式):任务信息(名称/任务号/简介)+ 业务链选择 +
|
||||||
|
* 执行设置(频率 / 总控提示 / 启用),提交后直接创建任务并完成绑定。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Radio } from "lucide-react";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { Button, Dialog, Input, Switch, Textarea } from "tdesign-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { listChains } from "@/roundtable-planning/api/chains";
|
||||||
|
import type { BusinessChain } from "@/roundtable-planning/lib/business-chain";
|
||||||
|
import { type ScheduleConfig } from "@/components/workspace/scheduled-tasks/schedule-builder";
|
||||||
|
|
||||||
|
import AgentFlow from "@/pages/official-multi-agent-tasks/components/AgentFlow";
|
||||||
|
import SchedulePicker from "@/pages/official-multi-agent-tasks/components/SchedulePicker";
|
||||||
|
import {
|
||||||
|
defaultScheduleConfig,
|
||||||
|
describeScheduleConfig,
|
||||||
|
} from "@/pages/official-multi-agent-tasks/utils";
|
||||||
|
import type { MatSeat } from "@/pages/official-multi-agent-tasks/types";
|
||||||
|
|
||||||
|
import { NO_CHAINS, SEED_CHAIN, chainSeats, chainStages } from "./BindingDialog";
|
||||||
|
import type { TaBindingPayload } from "../types";
|
||||||
|
|
||||||
|
export interface TaCreateTaskPayload {
|
||||||
|
name: string;
|
||||||
|
sourceTaskId?: string;
|
||||||
|
description?: string;
|
||||||
|
binding: TaBindingPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreateTaskDialog({
|
||||||
|
visible,
|
||||||
|
submitting,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
visible: boolean;
|
||||||
|
submitting: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (payload: TaCreateTaskPayload) => void;
|
||||||
|
}) {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [sourceTaskId, setSourceTaskId] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [chainId, setChainId] = useState("");
|
||||||
|
const [schedule, setSchedule] = useState<ScheduleConfig>(defaultScheduleConfig());
|
||||||
|
const [enabled, setEnabled] = useState(true);
|
||||||
|
const [coordinatorPrompt, setCoordinatorPrompt] = useState("");
|
||||||
|
|
||||||
|
const { data, isLoading: chainsLoading } = useQuery({
|
||||||
|
queryKey: ["ta", "chains"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const [mine, publicChains] = await Promise.all([
|
||||||
|
listChains("mine").catch(() => [] as BusinessChain[]),
|
||||||
|
listChains("public").catch(() => [] as BusinessChain[]),
|
||||||
|
]);
|
||||||
|
const byId = new Map<string, BusinessChain>();
|
||||||
|
for (const chain of [SEED_CHAIN, ...mine, ...publicChains]) {
|
||||||
|
if (!byId.has(chain.id)) byId.set(chain.id, chain);
|
||||||
|
}
|
||||||
|
return [...byId.values()];
|
||||||
|
},
|
||||||
|
enabled: visible,
|
||||||
|
});
|
||||||
|
const chains = data ?? NO_CHAINS;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) return;
|
||||||
|
setName("");
|
||||||
|
setSourceTaskId("");
|
||||||
|
setDescription("");
|
||||||
|
setSchedule(defaultScheduleConfig());
|
||||||
|
setEnabled(true);
|
||||||
|
setCoordinatorPrompt("");
|
||||||
|
setChainId((prev) => (chains.some((chain) => chain.id === prev) ? prev : chains[0]?.id ?? SEED_CHAIN.id));
|
||||||
|
}, [visible, chains]);
|
||||||
|
|
||||||
|
const selectedChain = useMemo(
|
||||||
|
() => chains.find((chain) => chain.id === chainId) ?? null,
|
||||||
|
[chains, chainId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const trySubmit = () => {
|
||||||
|
if (!name.trim()) {
|
||||||
|
toast.warning("请填写任务名称");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!selectedChain) {
|
||||||
|
toast.warning("请选择业务链");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSubmit({
|
||||||
|
name,
|
||||||
|
sourceTaskId: sourceTaskId.trim() || undefined,
|
||||||
|
description: description.trim() || undefined,
|
||||||
|
binding: {
|
||||||
|
chainId: selectedChain.id,
|
||||||
|
chainTitle: selectedChain.title,
|
||||||
|
schedule,
|
||||||
|
enabled,
|
||||||
|
coordinatorPrompt: coordinatorPrompt.trim() || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
className="ta-create-dialog"
|
||||||
|
closeBtn
|
||||||
|
closeOnOverlayClick={false}
|
||||||
|
destroyOnClose
|
||||||
|
footer={
|
||||||
|
<div className="ta-dialog-footer">
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button loading={submitting} theme="primary" onClick={trySubmit}>
|
||||||
|
创建任务并绑定
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
header="新建任务"
|
||||||
|
placement="top"
|
||||||
|
visible={visible}
|
||||||
|
width={860}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<div className="ta-binding">
|
||||||
|
<section className="ta-binding-section">
|
||||||
|
<header className="ta-binding-section-head">
|
||||||
|
<h4>任务信息</h4>
|
||||||
|
<span>由本页录入;接入对方系统后,推送创建的任务同样出现在列表中</span>
|
||||||
|
</header>
|
||||||
|
<div className="ta-create-grid">
|
||||||
|
<label className="ta-field">
|
||||||
|
<span className="ta-field-label">
|
||||||
|
任务名称<i className="ta-required">*</i>
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
clearable
|
||||||
|
maxlength={40}
|
||||||
|
placeholder="例如:东南亚舆论环境监测"
|
||||||
|
value={name}
|
||||||
|
onChange={(value) => setName(String(value ?? ""))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="ta-field">
|
||||||
|
<span className="ta-field-label">对方任务号(可选)</span>
|
||||||
|
<Input
|
||||||
|
clearable
|
||||||
|
maxlength={32}
|
||||||
|
placeholder="留空自动生成"
|
||||||
|
value={sourceTaskId}
|
||||||
|
onChange={(value) => setSourceTaskId(String(value ?? ""))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="ta-field ta-field--wide">
|
||||||
|
<span className="ta-field-label">任务简介</span>
|
||||||
|
<Textarea
|
||||||
|
autosize={{ minRows: 2, maxRows: 4 }}
|
||||||
|
maxlength={200}
|
||||||
|
placeholder="一句话说明任务背景与分析目标,定时执行时作为任务上下文注入各智能体"
|
||||||
|
value={description}
|
||||||
|
onChange={(value) => setDescription(String(value ?? ""))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="ta-binding-section">
|
||||||
|
<header className="ta-binding-section-head">
|
||||||
|
<h4>执行设置</h4>
|
||||||
|
<span>{describeScheduleConfig(schedule)} 自动执行</span>
|
||||||
|
</header>
|
||||||
|
<div className="ta-binding-settings">
|
||||||
|
<div className="ta-field">
|
||||||
|
<span className="ta-field-label">执行频率</span>
|
||||||
|
<SchedulePicker value={schedule} onChange={setSchedule} />
|
||||||
|
</div>
|
||||||
|
<label className="ta-field">
|
||||||
|
<span className="ta-field-label">总控提示(可选)</span>
|
||||||
|
<Textarea
|
||||||
|
autosize={{ minRows: 2, maxRows: 4 }}
|
||||||
|
maxlength={500}
|
||||||
|
placeholder="写给总控(协调者)的编排要求;问答沉淀的「方向备注」也会注入后续执行"
|
||||||
|
value={coordinatorPrompt}
|
||||||
|
onChange={(value) => setCoordinatorPrompt(String(value ?? ""))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="ta-field ta-field--switch">
|
||||||
|
<span className="ta-field-label">创建后立即启用</span>
|
||||||
|
<Switch value={enabled} onChange={(value) => setEnabled(!!value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="ta-binding-section">
|
||||||
|
<header className="ta-binding-section-head">
|
||||||
|
<h4>选择业务链</h4>
|
||||||
|
<span>定时执行按业务链的多智能体关系(同阶段并行 · 阶段间串行)运行</span>
|
||||||
|
</header>
|
||||||
|
{chainsLoading ? (
|
||||||
|
<p className="ta-binding-tip">正在加载业务链…</p>
|
||||||
|
) : (
|
||||||
|
<div className="ta-chain-grid">
|
||||||
|
{chains.map((chain) => {
|
||||||
|
const active = chain.id === chainId;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`ta-chain-card${active ? " is-active" : ""}`}
|
||||||
|
key={chain.id}
|
||||||
|
onClick={() => setChainId(chain.id)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<Radio aria-hidden="true" size={14} />
|
||||||
|
<span className="ta-chain-card-title">{chain.title}</span>
|
||||||
|
<em>
|
||||||
|
{chain.seats.length} 智能体 · {chainStages(chain).length} 阶段
|
||||||
|
</em>
|
||||||
|
</header>
|
||||||
|
{chain.description && <p>{chain.description}</p>}
|
||||||
|
<div className="ta-chain-card-flow">
|
||||||
|
<AgentFlow
|
||||||
|
seats={chainSeats(chain) as MatSeat[]}
|
||||||
|
size="sm"
|
||||||
|
stages={chainStages(chain)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* 席位执行过程抽屉:点击流程图节点打开,展示该智能体在某次运行里的完整过程
|
||||||
|
* (输入 → AI 分析 → 工具调用 → 产物块)。后端就绪后由 runs/{id}/messages?seat= 提供数据。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ArrowDownRight, Bot, Wrench } from "lucide-react";
|
||||||
|
import { Drawer, Tag } from "tdesign-react";
|
||||||
|
|
||||||
|
import { seatColorIndex } from "@/pages/official-multi-agent-tasks/utils";
|
||||||
|
import { buildSeatProcess } from "../store";
|
||||||
|
import type { TaRunView, TaSeatState, TaTask } from "../types";
|
||||||
|
|
||||||
|
const STATE_THEME: Record<TaSeatState, "success" | "warning" | "danger" | "default"> = {
|
||||||
|
done: "success",
|
||||||
|
running: "warning",
|
||||||
|
failed: "danger",
|
||||||
|
waiting: "default",
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATE_TEXT: Record<TaSeatState, string> = {
|
||||||
|
waiting: "等待执行",
|
||||||
|
running: "执行中",
|
||||||
|
done: "已完成",
|
||||||
|
failed: "执行失败",
|
||||||
|
};
|
||||||
|
|
||||||
|
const KIND_LABEL: Record<string, string> = {
|
||||||
|
input: "输入",
|
||||||
|
ai: "分析",
|
||||||
|
tool: "工具",
|
||||||
|
artifact: "产物",
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatDuration(ms: number): string {
|
||||||
|
if (!Number.isFinite(ms) || ms <= 0) return "—";
|
||||||
|
const seconds = Math.round(ms / 1000);
|
||||||
|
if (seconds < 60) return `${seconds} 秒`;
|
||||||
|
return `${Math.floor(seconds / 60)} 分 ${seconds % 60} 秒`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SeatProcessDrawer({
|
||||||
|
run,
|
||||||
|
seatAgentId,
|
||||||
|
task,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
run: TaRunView | null;
|
||||||
|
seatAgentId: string | null;
|
||||||
|
task: TaTask | undefined;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const visible = !!run && !!seatAgentId;
|
||||||
|
const process =
|
||||||
|
visible && run ? buildSeatProcess(run, seatAgentId!, task) : null;
|
||||||
|
const info = run?.seatInfos[seatAgentId ?? ""] ?? null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
className="ta-seat-drawer"
|
||||||
|
closeBtn
|
||||||
|
destroyOnClose
|
||||||
|
footer={null}
|
||||||
|
header={
|
||||||
|
process ? (
|
||||||
|
<div className="ta-seat-header">
|
||||||
|
<span className={`ta-seat-glyph ta-seat--c${seatColorIndex(seatAgentId ?? "")}`} aria-hidden="true">
|
||||||
|
<Bot size={18} />
|
||||||
|
</span>
|
||||||
|
<div className="ta-seat-heading">
|
||||||
|
<h3>{process.seatName}</h3>
|
||||||
|
<p>执行过程 · {run?.trigger === "manual" ? "手动执行(总控问答)" : "定时执行"}</p>
|
||||||
|
</div>
|
||||||
|
{info && (
|
||||||
|
<Tag size="small" theme={STATE_THEME[info.state]} variant="light">
|
||||||
|
{STATE_TEXT[info.state]}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
placement="right"
|
||||||
|
size="520px"
|
||||||
|
visible={visible}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
{process && (
|
||||||
|
<div className="ta-seat-process">
|
||||||
|
{info && (
|
||||||
|
<div className="ta-seat-meta">
|
||||||
|
<span>状态:{STATE_TEXT[info.state]}</span>
|
||||||
|
<span>耗时:{formatDuration(info.durationMs)}</span>
|
||||||
|
<span>所属阶段:第 {(run?.stages.findIndex((stage) => stage.includes(seatAgentId ?? "")) ?? -1) + 1} 阶段</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ol className="ta-seat-timeline">
|
||||||
|
{process.messages.map((message, index) => (
|
||||||
|
<li className={`ta-seat-item is-${message.kind}`} key={index}>
|
||||||
|
<span className="ta-seat-item-marker">
|
||||||
|
{message.kind === "tool" ? (
|
||||||
|
<Wrench size={11} />
|
||||||
|
) : (
|
||||||
|
<ArrowDownRight size={11} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<div className="ta-seat-item-body">
|
||||||
|
<header>
|
||||||
|
<em className={`ta-seat-item-kind is-${message.kind}`}>
|
||||||
|
{KIND_LABEL[message.kind] ?? message.kind}
|
||||||
|
</em>
|
||||||
|
<span className="ta-seat-item-title">{message.title}</span>
|
||||||
|
</header>
|
||||||
|
<p>{message.content}</p>
|
||||||
|
{message.detail && <pre>{message.detail}</pre>}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import type { TaRunView, TaTask } from "./types";
|
||||||
|
|
||||||
|
export type { TaRunView, TaTask };
|
||||||
|
export type TaTaskViewLike = { task: TaTask; runs: TaRunView[] };
|
||||||
@@ -0,0 +1,607 @@
|
|||||||
|
/**
|
||||||
|
* 外部任务定时分析的本地 mock 存储层(与 multi-agent-tasks 同款模式)。
|
||||||
|
*
|
||||||
|
* - 任务/绑定/运行记录落 localStorage;「立即执行 / 模拟手动执行」写入运行记录,
|
||||||
|
* 状态由 startedAt + durationMs 按当前时间推导(排队 → 按阶段推进 → 成功/失败),
|
||||||
|
* 配合 React Query 轮询获得无后端的"实时"流程图推进。
|
||||||
|
* - 席位执行过程消息在读取侧按席位生成(buildSeatProcess),不入库。
|
||||||
|
* - 对外接口(创建/删除/详情时间线/问答)就绪后替换本层。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { buildCron } from "@/components/workspace/scheduled-tasks/schedule-builder";
|
||||||
|
import { describeScheduleConfig } from "@/pages/official-multi-agent-tasks/utils";
|
||||||
|
import { uuid } from "@/core/utils/uuid";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
TaBlockMapping,
|
||||||
|
TaBinding,
|
||||||
|
TaBindingPayload,
|
||||||
|
TaRunRecord,
|
||||||
|
TaRunStatus,
|
||||||
|
TaRunView,
|
||||||
|
TaSeatRunInfo,
|
||||||
|
TaSeatState,
|
||||||
|
TaTask,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "deerflow.official.task-analysis.v1";
|
||||||
|
|
||||||
|
interface TaStore {
|
||||||
|
tasks: TaTask[];
|
||||||
|
runs: TaRunRecord[];
|
||||||
|
/** 上次绑定时选择的业务链 id(创建接口响应里的 default_chain_id 记忆)。 */
|
||||||
|
lastChainId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PENDING_MS = 2_000;
|
||||||
|
const STAGE_MS = 6_000;
|
||||||
|
|
||||||
|
/** 对方系统定义的详情块类型(mock 固定清单;接接口后由创建方传入)。 */
|
||||||
|
export const TA_BLOCK_TYPES: Array<{ key: string; name: string }> = [
|
||||||
|
{ key: "background", name: "任务背景" },
|
||||||
|
{ key: "dynamics", name: "动态分析" },
|
||||||
|
{ key: "risk", name: "风险研判" },
|
||||||
|
{ key: "action", name: "行动建议" },
|
||||||
|
{ key: "conclusion", name: "结论汇总" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function readStore(): TaStore {
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (raw) {
|
||||||
|
const parsed = JSON.parse(raw) as Partial<TaStore>;
|
||||||
|
if (Array.isArray(parsed.tasks)) {
|
||||||
|
return {
|
||||||
|
tasks: parsed.tasks as TaTask[],
|
||||||
|
runs: parsed.runs ?? [],
|
||||||
|
lastChainId: parsed.lastChainId ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 解析失败按首次访问处理。
|
||||||
|
}
|
||||||
|
const seeded = seedStore();
|
||||||
|
writeStore(seeded);
|
||||||
|
return seeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeStore(store: TaStore) {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(store));
|
||||||
|
} catch {
|
||||||
|
// 存储被禁用时静默降级为内存态。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mutateStore(mutator: (store: TaStore) => TaStore): TaStore {
|
||||||
|
const next = mutator(readStore());
|
||||||
|
writeStore(next);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hoursAgo(hours: number): string {
|
||||||
|
return new Date(Date.now() - hours * 3_600_000).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function seedStore(): TaStore {
|
||||||
|
const seats = {
|
||||||
|
collector: "全网舆情采集",
|
||||||
|
analyst: "舆情研判分析",
|
||||||
|
writer: "报告撰写助手",
|
||||||
|
verifier: "数据核查员",
|
||||||
|
forecaster: "趋势预测专家",
|
||||||
|
};
|
||||||
|
|
||||||
|
const tasks: TaTask[] = [
|
||||||
|
{
|
||||||
|
id: "ta-task-1",
|
||||||
|
sourceTaskId: "TS-2026-0147",
|
||||||
|
name: "东南亚舆论环境监测",
|
||||||
|
description: "对方系统推送的任务:持续跟踪东南亚重点渠道舆论动向,输出结构化分析详情。",
|
||||||
|
binding: {
|
||||||
|
chainId: "seed-chain-std",
|
||||||
|
chainTitle: "标准舆情分析链",
|
||||||
|
cron: "0 9 * * *",
|
||||||
|
cronText: "每天 09:00",
|
||||||
|
enabled: true,
|
||||||
|
blocks: [
|
||||||
|
{ key: "background", name: "任务背景", seatAgentId: "collector" },
|
||||||
|
{ key: "dynamics", name: "动态分析", seatAgentId: "collector" },
|
||||||
|
{ key: "risk", name: "风险研判", seatAgentId: "analyst" },
|
||||||
|
{ key: "action", name: "行动建议", seatAgentId: "analyst" },
|
||||||
|
{ key: "conclusion", name: "结论汇总", seatAgentId: "writer" },
|
||||||
|
],
|
||||||
|
coordinatorPrompt: "采集阶段允许独立检索;研判阶段需引用采集阶段的原始线索。",
|
||||||
|
directionNotes: ["重点关注宗教节日前后的舆论波动。"],
|
||||||
|
timezone: "Asia/Shanghai",
|
||||||
|
},
|
||||||
|
createdAt: hoursAgo(24 * 12),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ta-task-2",
|
||||||
|
sourceTaskId: "TS-2026-0152",
|
||||||
|
name: "竞品新品发布追踪",
|
||||||
|
description: "追踪 5 家核心竞品的新品发布与市场反应,按块更新详情内容。",
|
||||||
|
binding: {
|
||||||
|
chainId: "seed-chain-std",
|
||||||
|
chainTitle: "标准舆情分析链",
|
||||||
|
cron: "30 8 * * 1",
|
||||||
|
cronText: "每周一 08:30",
|
||||||
|
enabled: false,
|
||||||
|
blocks: [
|
||||||
|
{ key: "background", name: "任务背景", seatAgentId: "verifier" },
|
||||||
|
{ key: "dynamics", name: "动态分析", seatAgentId: "collector" },
|
||||||
|
{ key: "conclusion", name: "结论汇总", seatAgentId: "writer" },
|
||||||
|
],
|
||||||
|
directionNotes: [],
|
||||||
|
timezone: "Asia/Shanghai",
|
||||||
|
},
|
||||||
|
createdAt: hoursAgo(24 * 8),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ta-task-3",
|
||||||
|
sourceTaskId: "TS-2026-0160",
|
||||||
|
name: "行业政策周报编制",
|
||||||
|
description: "对方系统刚创建的任务,尚未配置分析方案。",
|
||||||
|
binding: null,
|
||||||
|
createdAt: hoursAgo(5),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const stdStages = [
|
||||||
|
["collector", "verifier"],
|
||||||
|
["analyst"],
|
||||||
|
["writer"],
|
||||||
|
];
|
||||||
|
const stdNames = seats;
|
||||||
|
|
||||||
|
const runs: TaRunRecord[] = [
|
||||||
|
{
|
||||||
|
id: uuid() as string,
|
||||||
|
taskId: "ta-task-1",
|
||||||
|
trigger: "schedule",
|
||||||
|
scheduledFor: hoursAgo(2.1),
|
||||||
|
startedAt: hoursAgo(2.1),
|
||||||
|
durationMs: PENDING_MS + stdStages.length * STAGE_MS,
|
||||||
|
stages: stdStages,
|
||||||
|
stageGoals: ["并行完成全网采集与数据核查", "研判风险与行动方向", "汇总各块结论"],
|
||||||
|
seatNames: stdNames,
|
||||||
|
failStage: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuid() as string,
|
||||||
|
taskId: "ta-task-1",
|
||||||
|
trigger: "manual",
|
||||||
|
scheduledFor: hoursAgo(20),
|
||||||
|
startedAt: hoursAgo(20),
|
||||||
|
durationMs: PENDING_MS + stdStages.length * STAGE_MS,
|
||||||
|
stages: stdStages,
|
||||||
|
stageGoals: ["并行完成全网采集与数据核查", "研判风险与行动方向", "汇总各块结论"],
|
||||||
|
seatNames: stdNames,
|
||||||
|
failStage: null,
|
||||||
|
manualSummary: "总控问答:要求研判阶段补充跨平台对比维度,已确认应用到后续执行。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuid() as string,
|
||||||
|
taskId: "ta-task-1",
|
||||||
|
trigger: "schedule",
|
||||||
|
scheduledFor: hoursAgo(26),
|
||||||
|
startedAt: hoursAgo(26),
|
||||||
|
durationMs: PENDING_MS + stdStages.length * STAGE_MS,
|
||||||
|
stages: stdStages,
|
||||||
|
stageGoals: ["并行完成全网采集与数据核查", "研判风险与行动方向", "汇总各块结论"],
|
||||||
|
seatNames: stdNames,
|
||||||
|
failStage: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuid() as string,
|
||||||
|
taskId: "ta-task-2",
|
||||||
|
trigger: "schedule",
|
||||||
|
scheduledFor: hoursAgo(24 * 3),
|
||||||
|
startedAt: hoursAgo(24 * 3),
|
||||||
|
durationMs: PENDING_MS + stdStages.length * STAGE_MS,
|
||||||
|
stages: stdStages,
|
||||||
|
stageGoals: ["并行完成全网采集与数据核查", "研判风险与行动方向", "汇总各块结论"],
|
||||||
|
seatNames: stdNames,
|
||||||
|
failStage: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return { tasks, runs, lastChainId: "seed-chain-std" };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 由记录 + 当前时间推导运行视图:各席位状态/耗时、块产出状态。 */
|
||||||
|
function resolveRunView(run: TaRunRecord, task: TaTask | undefined, now = Date.now()): TaRunView {
|
||||||
|
const startedAt = new Date(run.startedAt).getTime();
|
||||||
|
const elapsed = now - startedAt;
|
||||||
|
const stageTotal = Math.max(1, run.stages.length);
|
||||||
|
const duration = run.durationMs || PENDING_MS + stageTotal * STAGE_MS;
|
||||||
|
|
||||||
|
const buildSeatInfos = (
|
||||||
|
status: TaRunStatus,
|
||||||
|
activeStage: number,
|
||||||
|
): Record<string, TaSeatRunInfo> => {
|
||||||
|
const infos: Record<string, TaSeatRunInfo> = {};
|
||||||
|
run.stages.forEach((stage, index) => {
|
||||||
|
const stageStart = PENDING_MS + index * STAGE_MS;
|
||||||
|
const stageEnd = stageStart + STAGE_MS;
|
||||||
|
const finished = status === "succeeded" || (status === "failed" && index < activeStage);
|
||||||
|
const active = !finished && index === activeStage && status !== "pending";
|
||||||
|
const state: TaSeatState = finished
|
||||||
|
? "done"
|
||||||
|
: active
|
||||||
|
? status === "failed"
|
||||||
|
? "failed"
|
||||||
|
: "running"
|
||||||
|
: "waiting";
|
||||||
|
const durationMs = finished
|
||||||
|
? Math.min(elapsed - stageStart, STAGE_MS)
|
||||||
|
: active
|
||||||
|
? Math.min(elapsed - stageStart, STAGE_MS)
|
||||||
|
: 0;
|
||||||
|
stage.forEach((agentId) => {
|
||||||
|
infos[agentId] = { state, durationMs };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return infos;
|
||||||
|
};
|
||||||
|
|
||||||
|
let status: TaRunStatus;
|
||||||
|
let stageIndex: number;
|
||||||
|
let finishedAt: string | null = null;
|
||||||
|
let error: string | null = null;
|
||||||
|
|
||||||
|
if (elapsed < PENDING_MS) {
|
||||||
|
status = "pending";
|
||||||
|
stageIndex = 0;
|
||||||
|
} else {
|
||||||
|
const progress = Math.min(elapsed - PENDING_MS, duration - PENDING_MS);
|
||||||
|
stageIndex = Math.min(Math.floor(progress / STAGE_MS), stageTotal - 1);
|
||||||
|
if (run.failStage !== null && stageIndex >= run.failStage) {
|
||||||
|
status = "failed";
|
||||||
|
stageIndex = run.failStage;
|
||||||
|
finishedAt = new Date(startedAt + PENDING_MS + (run.failStage + 1) * STAGE_MS).toISOString();
|
||||||
|
const failSeat = run.stages[run.failStage]?.[0];
|
||||||
|
error = `阶段 ${run.failStage + 1} 执行失败:${run.seatNames[failSeat ?? ""] ?? "席位"} 模拟执行异常`;
|
||||||
|
} else if (elapsed >= duration) {
|
||||||
|
status = "succeeded";
|
||||||
|
stageIndex = stageTotal - 1;
|
||||||
|
finishedAt = new Date(startedAt + duration).toISOString();
|
||||||
|
} else {
|
||||||
|
status = "running";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const seatInfos = buildSeatInfos(status, status === "pending" ? -1 : stageIndex);
|
||||||
|
|
||||||
|
const blockStates = (task?.binding?.blocks ?? []).map((block) => ({
|
||||||
|
key: block.key,
|
||||||
|
name: block.name,
|
||||||
|
state: seatInfos[block.seatAgentId]?.state ?? "waiting",
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...run,
|
||||||
|
finishedAt,
|
||||||
|
status,
|
||||||
|
stageIndex,
|
||||||
|
stageTotal,
|
||||||
|
seatInfos,
|
||||||
|
error,
|
||||||
|
blockStates,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTaskViews(now = Date.now()): Array<{ task: TaTask; runs: TaRunView[] }> {
|
||||||
|
const store = readStore();
|
||||||
|
return store.tasks.map((task) => {
|
||||||
|
const runs = store.runs
|
||||||
|
.filter((run) => run.taskId === task.id)
|
||||||
|
.map((run) => resolveRunView(run, task, now))
|
||||||
|
.sort((a, b) => (a.startedAt < b.startedAt ? 1 : -1));
|
||||||
|
return { task, runs };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function anyRunLive(views: Array<{ runs: TaRunView[] }> | undefined): boolean {
|
||||||
|
return !!views?.some(({ runs }) =>
|
||||||
|
runs.some((run) => run.status === "pending" || run.status === "running"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表 + 每任务运行视图;运行中 5 秒轮询(设计定稿:流程图每 5 秒刷新)。 */
|
||||||
|
export function useTaTasks() {
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ["ta", "tasks"],
|
||||||
|
queryFn: () => buildTaskViews(),
|
||||||
|
refetchInterval: (q) =>
|
||||||
|
anyRunLive(q.state.data as Array<{ runs: TaRunView[] }> | undefined) ? 5_000 : false,
|
||||||
|
});
|
||||||
|
const views = query.data ?? [];
|
||||||
|
return { views, tasks: views.map((view) => view.task), isLoading: query.isLoading };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTaTaskRuns(taskId: string | null) {
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ["ta", "tasks", taskId],
|
||||||
|
queryFn: () => {
|
||||||
|
const view = buildTaskViews().find((item) => item.task.id === taskId);
|
||||||
|
return view?.runs ?? [];
|
||||||
|
},
|
||||||
|
enabled: !!taskId,
|
||||||
|
refetchInterval: (q) => {
|
||||||
|
const runs = q.state.data as TaRunView[] | undefined;
|
||||||
|
const live = !!runs?.some((run) => run.status === "pending" || run.status === "running");
|
||||||
|
return live ? 5_000 : false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { runs: query.data ?? [], isLoading: query.isLoading };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTaTaskActions() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const invalidate = () => queryClient.invalidateQueries({ queryKey: ["ta"] });
|
||||||
|
|
||||||
|
/** 页面手动创建外部任务(名称必填;对方任务号留空则自动生成)。 */
|
||||||
|
const createTask = useMutation({
|
||||||
|
mutationFn: async (payload: {
|
||||||
|
name: string;
|
||||||
|
sourceTaskId?: string;
|
||||||
|
description?: string;
|
||||||
|
}) => {
|
||||||
|
const task: TaTask = {
|
||||||
|
id: uuid() as string,
|
||||||
|
sourceTaskId:
|
||||||
|
payload.sourceTaskId?.trim() ||
|
||||||
|
`TS-${new Date().getFullYear()}-${String(Date.now()).slice(-5)}`,
|
||||||
|
name: payload.name.trim(),
|
||||||
|
description: payload.description?.trim() || "",
|
||||||
|
binding: null,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
mutateStore((store) => ({ ...store, tasks: [task, ...store.tasks] }));
|
||||||
|
return task;
|
||||||
|
},
|
||||||
|
onSuccess: invalidate,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 绑定 / 修改绑定(记录 lastChainId 供创建接口返回默认项)。 */
|
||||||
|
const bind = useMutation({
|
||||||
|
mutationFn: async ({ taskId, payload }: { taskId: string; payload: TaBindingPayload }) => {
|
||||||
|
const cron = buildCron(payload.schedule);
|
||||||
|
mutateStore((store) => ({
|
||||||
|
...store,
|
||||||
|
lastChainId: payload.chainId,
|
||||||
|
tasks: store.tasks.map((task) =>
|
||||||
|
task.id === taskId
|
||||||
|
? {
|
||||||
|
...task,
|
||||||
|
binding: {
|
||||||
|
chainId: payload.chainId,
|
||||||
|
chainTitle: payload.chainTitle,
|
||||||
|
cron,
|
||||||
|
cronText: describeScheduleConfig(payload.schedule),
|
||||||
|
enabled: payload.enabled,
|
||||||
|
blocks: payload.blocks ?? task.binding?.blocks ?? [],
|
||||||
|
coordinatorPrompt: payload.coordinatorPrompt,
|
||||||
|
directionNotes: task.binding?.directionNotes ?? [],
|
||||||
|
timezone: task.binding?.timezone ?? "Asia/Shanghai",
|
||||||
|
} satisfies TaBinding,
|
||||||
|
}
|
||||||
|
: task,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
onSuccess: invalidate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const unbind = useMutation({
|
||||||
|
mutationFn: async (taskId: string) => {
|
||||||
|
mutateStore((store) => ({
|
||||||
|
...store,
|
||||||
|
tasks: store.tasks.map((task) =>
|
||||||
|
task.id === taskId ? { ...task, binding: null } : task,
|
||||||
|
),
|
||||||
|
runs: store.runs.filter((run) => run.taskId !== taskId),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
onSuccess: invalidate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const setEnabled = useMutation({
|
||||||
|
mutationFn: async ({ taskId, enabled }: { taskId: string; enabled: boolean }) => {
|
||||||
|
mutateStore((store) => ({
|
||||||
|
...store,
|
||||||
|
tasks: store.tasks.map((task) =>
|
||||||
|
task.id === taskId && task.binding
|
||||||
|
? { ...task, binding: { ...task.binding, enabled } }
|
||||||
|
: task,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
onSuccess: invalidate,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 删除外部任务(对应对外删除接口:任务 + 绑定 + 执行记录彻底删除)。 */
|
||||||
|
const removeTask = useMutation({
|
||||||
|
mutationFn: async (taskId: string) => {
|
||||||
|
mutateStore((store) => ({
|
||||||
|
...store,
|
||||||
|
tasks: store.tasks.filter((task) => task.id !== taskId),
|
||||||
|
runs: store.runs.filter((run) => run.taskId !== taskId),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
onSuccess: invalidate,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 立即触发一次定时执行(schedule)。 */
|
||||||
|
const triggerRun = useMutation({
|
||||||
|
mutationFn: async (taskId: string) => {
|
||||||
|
const store = readStore();
|
||||||
|
const task = store.tasks.find((item) => item.id === taskId);
|
||||||
|
const binding = task?.binding;
|
||||||
|
if (!task || !binding) throw new Error("任务未绑定分析方案");
|
||||||
|
const stages = chainStagesOf(binding, store);
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const run: TaRunRecord = {
|
||||||
|
id: uuid() as string,
|
||||||
|
taskId,
|
||||||
|
trigger: "schedule",
|
||||||
|
scheduledFor: now,
|
||||||
|
startedAt: now,
|
||||||
|
durationMs: PENDING_MS + stages.length * STAGE_MS,
|
||||||
|
stages,
|
||||||
|
stageGoals: stages.map((_, index) => stageGoalOf(index)),
|
||||||
|
seatNames: Object.fromEntries(stages.flat().map((id) => [id, seatDisplayName(id)])),
|
||||||
|
failStage: null,
|
||||||
|
};
|
||||||
|
mutateStore((current) => ({ ...current, runs: [run, ...current.runs] }));
|
||||||
|
return run;
|
||||||
|
},
|
||||||
|
onSuccess: invalidate,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 模拟一次手动执行(对方详情页总控问答产生)。 */
|
||||||
|
const triggerManualRun = useMutation({
|
||||||
|
mutationFn: async (taskId: string) => {
|
||||||
|
const store = readStore();
|
||||||
|
const task = store.tasks.find((item) => item.id === taskId);
|
||||||
|
const binding = task?.binding;
|
||||||
|
if (!task || !binding) throw new Error("任务未绑定分析方案");
|
||||||
|
const stages = chainStagesOf(binding, store);
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const run: TaRunRecord = {
|
||||||
|
id: uuid() as string,
|
||||||
|
taskId,
|
||||||
|
trigger: "manual",
|
||||||
|
scheduledFor: now,
|
||||||
|
startedAt: now,
|
||||||
|
durationMs: PENDING_MS + stages.length * STAGE_MS,
|
||||||
|
stages,
|
||||||
|
stageGoals: stages.map((_, index) => stageGoalOf(index)),
|
||||||
|
seatNames: Object.fromEntries(stages.flat().map((id) => [id, seatDisplayName(id)])),
|
||||||
|
failStage: null,
|
||||||
|
manualSummary: "总控问答:按对方详情页提问,总控给对应席位派活并汇总结论。",
|
||||||
|
};
|
||||||
|
mutateStore((current) => ({ ...current, runs: [run, ...current.runs] }));
|
||||||
|
return run;
|
||||||
|
},
|
||||||
|
onSuccess: invalidate,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { createTask, bind, unbind, setEnabled, removeTask, triggerRun, triggerManualRun };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绑定链的编排(种子任务用内置编排;真实业务链按其 stages 展开,null = 线性)。 */
|
||||||
|
export function chainStagesOf(
|
||||||
|
binding: Pick<TaBinding, "chainId">,
|
||||||
|
store: TaStore = readStore(),
|
||||||
|
): string[][] {
|
||||||
|
if (binding.chainId === "seed-chain-std") {
|
||||||
|
return [
|
||||||
|
["collector", "verifier"],
|
||||||
|
["analyst"],
|
||||||
|
["writer"],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// 真实业务链的编排由页面侧传入的 chain 数据展开;存储层兜底为单席位串行。
|
||||||
|
const seats = seedSeatsOf(store);
|
||||||
|
return seats.map((agentId) => [agentId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function seedSeatsOf(_store: TaStore): string[] {
|
||||||
|
return ["collector", "verifier", "analyst", "writer"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 演示用的阶段目标(seed 链固定;真实链由绑定快照提供)。 */
|
||||||
|
function stageGoalOf(stageIndex: number): string | null {
|
||||||
|
const goals = ["并行完成全网采集与数据核查", "研判风险与行动方向", "汇总各块结论"];
|
||||||
|
return goals[stageIndex] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function seatDisplayName(agentId: string): string {
|
||||||
|
const names: Record<string, string> = {
|
||||||
|
collector: "全网舆情采集",
|
||||||
|
verifier: "数据核查员",
|
||||||
|
analyst: "舆情研判分析",
|
||||||
|
writer: "报告撰写助手",
|
||||||
|
forecaster: "趋势预测专家",
|
||||||
|
};
|
||||||
|
return names[agentId] ?? agentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 席位执行过程(读取侧生成 mock)────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface TaSeatMessage {
|
||||||
|
kind: "input" | "ai" | "tool" | "artifact";
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
detail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成某席位在某次运行里的执行过程消息(后端就绪后换 runs/{id}/messages?seat=)。 */
|
||||||
|
export function buildSeatProcess(
|
||||||
|
run: TaRunView,
|
||||||
|
seatAgentId: string,
|
||||||
|
task: TaTask | undefined,
|
||||||
|
): { seatName: string; messages: TaSeatMessage[]; blocks: TaBlockMapping[] } {
|
||||||
|
const seatName = run.seatNames[seatAgentId] ?? seatAgentId;
|
||||||
|
const info = run.seatInfos[seatAgentId] ?? { state: "waiting" as TaSeatState, durationMs: 0 };
|
||||||
|
const stageIndex = run.stages.findIndex((stage) => stage.includes(seatAgentId));
|
||||||
|
const blocks = (task?.binding?.blocks ?? []).filter(
|
||||||
|
(block) => block.seatAgentId === seatAgentId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const messages: TaSeatMessage[] = [
|
||||||
|
{
|
||||||
|
kind: "input",
|
||||||
|
title: "输入 · 任务上下文",
|
||||||
|
content:
|
||||||
|
`${task?.name ?? ""}(对方任务 ${task?.sourceTaskId ?? "-"})\n` +
|
||||||
|
`${task?.description ?? ""}\n\n` +
|
||||||
|
(blocks.length
|
||||||
|
? `本席位负责产出:${blocks.map((block) => block.name).join("、")}。`
|
||||||
|
: "按业务链席位职责执行。") +
|
||||||
|
(stageIndex > 0
|
||||||
|
? `\n上一阶段产出摘要:${run.stages[stageIndex - 1]
|
||||||
|
?.map((id) => run.seatNames[id])
|
||||||
|
.join("、")} 的中间结论已注入。`
|
||||||
|
: ""),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (info.state === "waiting") {
|
||||||
|
return { seatName, messages, blocks };
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.push({
|
||||||
|
kind: "ai",
|
||||||
|
title: "AI 分析",
|
||||||
|
content:
|
||||||
|
`(${seatName} 的分析过程 · 演示内容)\n` +
|
||||||
|
`围绕任务简介拆解检索方向,交叉核对来源可信度,形成本席位负责块的结构化结论。`,
|
||||||
|
});
|
||||||
|
|
||||||
|
messages.push({
|
||||||
|
kind: "tool",
|
||||||
|
title: "工具调用 · web_search",
|
||||||
|
content: "query=\"任务关键词 最新动态\" → 命中 12 条,采纳 7 条(含来源与时间)",
|
||||||
|
detail:
|
||||||
|
'{"results":[{"title":"来源A:行业动态","snippet":"……"},{"title":"来源B:政策更新","snippet":"……"}]}',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (info.state === "done") {
|
||||||
|
blocks.forEach((block) => {
|
||||||
|
messages.push({
|
||||||
|
kind: "artifact",
|
||||||
|
title: `产出 · ${block.name}`,
|
||||||
|
content: `(${block.name} 块内容摘要 · 演示)已按对方系统约定的结构生成,随本次执行写入时间线版本。`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { seatName, messages, blocks };
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* 外部任务多智能体定时分析 —— 前端先行数据模型(后端接口就绪后替换 store 层)。
|
||||||
|
*
|
||||||
|
* 对应设计定稿:
|
||||||
|
* - 外部任务(对方系统推送创建/删除)1:1 绑定「业务链 + 频率 + 块→席位映射」。
|
||||||
|
* - 执行记录两类:schedule 定时执行 / manual 手动执行(对方详情页总控问答)。
|
||||||
|
* - 时间线内容版本:每次定时执行产出各块内容(供对方详情接口拉取),管理页内
|
||||||
|
* 在席位执行过程抽屉里预览。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ScheduleConfig } from "@/components/workspace/scheduled-tasks/schedule-builder";
|
||||||
|
|
||||||
|
export type TaRunTrigger = "schedule" | "manual";
|
||||||
|
export type TaRunStatus = "pending" | "running" | "succeeded" | "failed";
|
||||||
|
export type TaSeatState = "waiting" | "running" | "done" | "failed";
|
||||||
|
|
||||||
|
/** 对方详情的分块内容与业务链席位的映射(存在绑定上,业务链保持纯粹)。 */
|
||||||
|
export interface TaBlockMapping {
|
||||||
|
/** 块类型 key(对方系统定义)。 */
|
||||||
|
key: string;
|
||||||
|
/** 块展示名。 */
|
||||||
|
name: string;
|
||||||
|
/** 产出该块的席位(业务链 agent_id)。 */
|
||||||
|
seatAgentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaBinding {
|
||||||
|
chainId: string;
|
||||||
|
/** 业务链标题快照(展示用,链被删后仍可读)。 */
|
||||||
|
chainTitle: string;
|
||||||
|
cron: string;
|
||||||
|
cronText: string;
|
||||||
|
enabled: boolean;
|
||||||
|
blocks: TaBlockMapping[];
|
||||||
|
coordinatorPrompt?: string;
|
||||||
|
/** 问答沉淀的「执行方向备注」,注入后续每次定时执行(累积追加)。 */
|
||||||
|
directionNotes: string[];
|
||||||
|
/** 绑定时记住,作为下次创建任务的默认业务链返回给对方系统。 */
|
||||||
|
timezone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaTask {
|
||||||
|
id: string;
|
||||||
|
/** 对方系统任务 id。 */
|
||||||
|
sourceTaskId: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
binding: TaBinding | null;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 落库的运行记录;状态由 startedAt + durationMs 在读取时按当前时间推导。 */
|
||||||
|
export interface TaRunRecord {
|
||||||
|
id: string;
|
||||||
|
taskId: string;
|
||||||
|
trigger: TaRunTrigger;
|
||||||
|
scheduledFor: string;
|
||||||
|
startedAt: string;
|
||||||
|
durationMs: number;
|
||||||
|
/** 该次执行的业务链编排快照(席位/阶段),链后续修改不影响历史回放。 */
|
||||||
|
stages: string[][];
|
||||||
|
/** 各阶段目标快照(与 stages 同下标),供流程图连线标签展示。 */
|
||||||
|
stageGoals?: (string | null)[] | null;
|
||||||
|
seatNames: Record<string, string>;
|
||||||
|
/** 在该阶段(下标)失败;null = 成功。 */
|
||||||
|
failStage: number | null;
|
||||||
|
/** 手动执行(总控问答)的摘要。 */
|
||||||
|
manualSummary?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读取时推导出的运行视图(每席位状态 + 耗时)。 */
|
||||||
|
export interface TaSeatRunInfo {
|
||||||
|
state: TaSeatState;
|
||||||
|
durationMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaRunView {
|
||||||
|
id: string;
|
||||||
|
taskId: string;
|
||||||
|
trigger: TaRunTrigger;
|
||||||
|
scheduledFor: string;
|
||||||
|
startedAt: string;
|
||||||
|
finishedAt: string | null;
|
||||||
|
status: TaRunStatus;
|
||||||
|
stageIndex: number;
|
||||||
|
stageTotal: number;
|
||||||
|
seatInfos: Record<string, TaSeatRunInfo>;
|
||||||
|
error: string | null;
|
||||||
|
/** 各块产出状态(按绑定块映射推导;未绑定返回空)。 */
|
||||||
|
blockStates: Array<{ key: string; name: string; state: TaSeatState }>;
|
||||||
|
manualSummary?: string;
|
||||||
|
stages: string[][];
|
||||||
|
stageGoals?: (string | null)[] | null;
|
||||||
|
seatNames: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绑定弹窗的提交载荷(内容块映射已移除;blocks 仅为兼容旧数据保留)。 */
|
||||||
|
export interface TaBindingPayload {
|
||||||
|
chainId: string;
|
||||||
|
chainTitle: string;
|
||||||
|
schedule: ScheduleConfig;
|
||||||
|
enabled: boolean;
|
||||||
|
blocks?: TaBlockMapping[];
|
||||||
|
coordinatorPrompt?: string;
|
||||||
|
}
|
||||||
@@ -39,11 +39,16 @@ export interface ChainFlowStage {
|
|||||||
goal?: string | null;
|
goal?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 节点执行状态(任务分析回放传入;缺省展示 Ready)。 */
|
||||||
|
export type ChainFlowSeatState = "waiting" | "running" | "done" | "failed";
|
||||||
|
|
||||||
export interface ChainFlowDiagramProps {
|
export interface ChainFlowDiagramProps {
|
||||||
stages: ChainFlowStage[];
|
stages: ChainFlowStage[];
|
||||||
/** 点击席位节点卡片(总控节点不触发)。 */
|
/** 点击席位节点卡片(总控节点不触发)。 */
|
||||||
onNodeClick?: (agentId: string, name?: string) => void;
|
onNodeClick?: (agentId: string, name?: string) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/** 传入时节点卡片叠加执行状态徽标(任务分析的运行回放);不影响原有展示。 */
|
||||||
|
seatStates?: Record<string, { state: ChainFlowSeatState; durationMs?: number }> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 布局常量(世界坐标,单位 px)─────────────────────────────────────────────
|
// ── 布局常量(世界坐标,单位 px)─────────────────────────────────────────────
|
||||||
@@ -84,7 +89,7 @@ interface PlacedNode {
|
|||||||
rightX: number;
|
rightX: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChainFlowDiagram({ stages, onNodeClick, className }: ChainFlowDiagramProps) {
|
export function ChainFlowDiagram({ stages, onNodeClick, className, seatStates }: ChainFlowDiagramProps) {
|
||||||
// ── 列 / 节点 / 连线 / 标签布局 ──────────────────────────────────────────
|
// ── 列 / 节点 / 连线 / 标签布局 ──────────────────────────────────────────
|
||||||
const layout = useMemo(() => {
|
const layout = useMemo(() => {
|
||||||
// 列:总控 + 各 Stage。
|
// 列:总控 + 各 Stage。
|
||||||
@@ -315,15 +320,43 @@ export function ChainFlowDiagram({ stages, onNodeClick, className }: ChainFlowDi
|
|||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
{/* 节点卡片(参考「业务链条配置器」样式:左彩色描边 + 头像 + 名称 +
|
{/* 节点卡片(参考「业务链条配置器」样式:左彩色描边 + 头像 + 名称 +
|
||||||
L-阶段徽标 + 同色角色行 + 底部 Ready 心跳条;图标仍用我方 getAvatar)。 */}
|
L-阶段徽标 + 同色角色行 + 底部 Ready 心跳条;图标仍用我方 getAvatar)。
|
||||||
|
seatStates 传入时(任务分析回放)叠加执行状态徽标与状态描边。 */}
|
||||||
{layout.cols.flatMap((col, colIdx) =>
|
{layout.cols.flatMap((col, colIdx) =>
|
||||||
col.nodes.map((n) => {
|
col.nodes.map((n) => {
|
||||||
const isCoordinator = n.id === COORDINATOR_ID;
|
const isCoordinator = n.id === COORDINATOR_ID;
|
||||||
const nc = nodeColorOf(n.avatarType);
|
const nc = nodeColorOf(n.avatarType);
|
||||||
|
const seatState = !isCoordinator && seatStates ? seatStates[n.id] : undefined;
|
||||||
|
const state = seatState?.state;
|
||||||
|
const stateStyle = state
|
||||||
|
? ({
|
||||||
|
waiting: "text-muted-foreground",
|
||||||
|
running: "text-amber-600 dark:text-amber-300",
|
||||||
|
done: "text-emerald-600 dark:text-emerald-300",
|
||||||
|
failed: "text-red-600 dark:text-red-400",
|
||||||
|
} as const)[state]
|
||||||
|
: null;
|
||||||
|
const stateLabel =
|
||||||
|
state === "waiting"
|
||||||
|
? "等待"
|
||||||
|
: state === "running"
|
||||||
|
? "执行中"
|
||||||
|
: state === "done"
|
||||||
|
? "完成"
|
||||||
|
: state === "failed"
|
||||||
|
? "失败"
|
||||||
|
: null;
|
||||||
|
const stateSeconds = seatState?.durationMs
|
||||||
|
? Math.max(1, Math.round(seatState.durationMs / 1000))
|
||||||
|
: null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={n.id}
|
key={n.id}
|
||||||
className={cn("absolute", !isCoordinator && "cursor-pointer")}
|
className={cn(
|
||||||
|
"absolute",
|
||||||
|
!isCoordinator && "cursor-pointer",
|
||||||
|
state === "waiting" && "opacity-60",
|
||||||
|
)}
|
||||||
style={{ left: n.x, top: n.y, width: NODE_W, height: NODE_H }}
|
style={{ left: n.x, top: n.y, width: NODE_W, height: NODE_H }}
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -339,9 +372,15 @@ export function ChainFlowDiagram({ stages, onNodeClick, className }: ChainFlowDi
|
|||||||
isCoordinator
|
isCoordinator
|
||||||
? "border-border bg-primary/[0.04]"
|
? "border-border bg-primary/[0.04]"
|
||||||
: "border-border hover:-translate-y-0.5 hover:shadow-[0_8px_22px_rgba(15,23,42,0.10)]",
|
: "border-border hover:-translate-y-0.5 hover:shadow-[0_8px_22px_rgba(15,23,42,0.10)]",
|
||||||
|
state === "running" && "border-amber-400/80",
|
||||||
|
state === "failed" && "border-red-500/70",
|
||||||
)}
|
)}
|
||||||
// 左侧 4px 彩色描边:内联上色,避免被 border-border 的四边色覆盖。
|
// 左侧 4px 彩色描边:内联上色,避免被 border-border 的四边色覆盖。
|
||||||
style={{ borderLeftColor: nc.hex, borderLeftWidth: 4 }}
|
style={{
|
||||||
|
borderLeftColor: nc.hex,
|
||||||
|
borderLeftWidth: 4,
|
||||||
|
...(state === "running" ? { boxShadow: "0 0 0 3px rgba(245,158,11,0.15)" } : {}),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* 上:头像 + 名称 + 阶段徽标 + 角色行 */}
|
{/* 上:头像 + 名称 + 阶段徽标 + 角色行 */}
|
||||||
<div className="flex items-start gap-2 overflow-hidden">
|
<div className="flex items-start gap-2 overflow-hidden">
|
||||||
@@ -359,13 +398,34 @@ export function ChainFlowDiagram({ stages, onNodeClick, className }: ChainFlowDi
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 下:身份 + Ready 心跳条 */}
|
{/* 下:身份 + 状态行(回放模式显示执行状态,否则 Ready 心跳条) */}
|
||||||
<div className="flex items-center justify-between border-t border-border/60 pt-1 text-[8px] text-muted-foreground">
|
<div className="flex items-center justify-between border-t border-border/60 pt-1 text-[8px] text-muted-foreground">
|
||||||
<span className="truncate">{isCoordinator ? "总控协调核心" : "智能体席位"}</span>
|
<span className="truncate">{isCoordinator ? "总控协调核心" : "智能体席位"}</span>
|
||||||
|
{state && stateLabel ? (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"ml-1.5 flex shrink-0 items-center gap-1 rounded px-1 py-0.5 font-medium",
|
||||||
|
stateStyle,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"size-1.5 rounded-full",
|
||||||
|
state === "done" && "bg-emerald-500",
|
||||||
|
state === "running" && "animate-pulse bg-amber-500",
|
||||||
|
state === "failed" && "bg-red-500",
|
||||||
|
state === "waiting" && "bg-muted-foreground/60",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{stateLabel}
|
||||||
|
{stateSeconds != null && state !== "waiting" ? ` · ${stateSeconds}s` : ""}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
<span className="ml-1.5 flex shrink-0 items-center gap-1 rounded bg-muted px-1 py-0.5 font-medium text-primary">
|
<span className="ml-1.5 flex shrink-0 items-center gap-1 rounded bg-muted px-1 py-0.5 font-medium text-primary">
|
||||||
<span className="size-1 rounded-full bg-emerald-500" />
|
<span className="size-1 rounded-full bg-emerald-500" />
|
||||||
Ready
|
Ready
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -445,13 +505,23 @@ export function ChainFlowDiagram({ stages, onNodeClick, className }: ChainFlowDi
|
|||||||
style={{ width: mmW, height: mmH }}
|
style={{ width: mmW, height: mmH }}
|
||||||
onClick={onMinimapClick}
|
onClick={onMinimapClick}
|
||||||
>
|
>
|
||||||
{/* 节点缩略块 */}
|
{/* 节点缩略块(回放模式按状态着色) */}
|
||||||
{layout.allNodes.map((n) => (
|
{layout.allNodes.map((n) => {
|
||||||
|
const state = n.id === COORDINATOR_ID ? null : seatStates?.[n.id]?.state ?? null;
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={n.id}
|
key={n.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute rounded-[1px]",
|
"absolute rounded-[1px]",
|
||||||
n.id === COORDINATOR_ID ? "bg-primary/70" : "bg-foreground/35",
|
n.id === COORDINATOR_ID
|
||||||
|
? "bg-primary/70"
|
||||||
|
: state === "done"
|
||||||
|
? "bg-emerald-500/70"
|
||||||
|
: state === "running"
|
||||||
|
? "bg-amber-500/80"
|
||||||
|
: state === "failed"
|
||||||
|
? "bg-red-500/80"
|
||||||
|
: "bg-foreground/35",
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
left: n.x * mmScale,
|
left: n.x * mmScale,
|
||||||
@@ -460,7 +530,8 @@ export function ChainFlowDiagram({ stages, onNodeClick, className }: ChainFlowDi
|
|||||||
height: NODE_H * mmScale,
|
height: NODE_H * mmScale,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
{/* 当前视口框 */}
|
{/* 当前视口框 */}
|
||||||
<div
|
<div
|
||||||
className="absolute border-2 border-primary bg-primary/10"
|
className="absolute border-2 border-primary bg-primary/10"
|
||||||
|
|||||||
@@ -456,7 +456,10 @@ auth_login:
|
|||||||
# 点击后在新标签页打开下面的 url。enabled 关掉则不显示该入口。
|
# 点击后在新标签页打开下面的 url。enabled 关掉则不显示该入口。
|
||||||
|
|
||||||
database:
|
database:
|
||||||
backend: sqlite
|
# 本地业务数据使用远程 MySQL;连接地址由 backend/.env.local 提供。
|
||||||
|
# LangGraph 对话检查点仍由下方 checkpointer 保存到本机 SQLite。
|
||||||
|
backend: mysql
|
||||||
|
mysql_url: $MYSQL_DATABASE_URL
|
||||||
sqlite_dir: .deer-flow/data
|
sqlite_dir: .deer-flow/data
|
||||||
# 高斯/OpenGauss 业务库示例(启用前先执行 uv sync --extra gauss):
|
# 高斯/OpenGauss 业务库示例(启用前先执行 uv sync --extra gauss):
|
||||||
# backend: gauss
|
# backend: gauss
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ BUSINESS_TABLES = [
|
|||||||
"feedback",
|
"feedback",
|
||||||
"agents",
|
"agents",
|
||||||
"skills",
|
"skills",
|
||||||
|
"ai_writing_article_types",
|
||||||
|
"ai_writing_sessions",
|
||||||
"scheduled_tasks",
|
"scheduled_tasks",
|
||||||
"scheduled_task_subscriptions",
|
"scheduled_task_subscriptions",
|
||||||
"scheduled_task_runs",
|
"scheduled_task_runs",
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Starts the SSH tunnel required by the local DeerFlow backend's remote MySQL connection.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
The local backend is configured to reach MySQL at 127.0.0.1:13307. This script forwards
|
||||||
|
that port over SSH to the MySQL listener on the remote server. It deliberately does not
|
||||||
|
store the SSH password; OpenSSH prompts for it when the tunnel starts.
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$RemoteHost = "119.254.86.251",
|
||||||
|
[int]$SshPort = 22322,
|
||||||
|
[string]$SshUser = "root",
|
||||||
|
[int]$LocalPort = 13307,
|
||||||
|
[int]$RemoteMySqlPort = 3307
|
||||||
|
)
|
||||||
|
|
||||||
|
$existing = Get-NetTCPConnection -State Listen -LocalPort $LocalPort -ErrorAction SilentlyContinue
|
||||||
|
if ($existing) {
|
||||||
|
Write-Host "本地端口 $LocalPort 已在监听;SSH 隧道应已启动。" -ForegroundColor Yellow
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "正在建立 MySQL SSH 隧道:127.0.0.1:$LocalPort -> $RemoteHost`:$RemoteMySqlPort"
|
||||||
|
Write-Host "请保持此窗口运行;按 Ctrl+C 可关闭隧道。"
|
||||||
|
|
||||||
|
ssh -N `
|
||||||
|
-o ExitOnForwardFailure=yes `
|
||||||
|
-o ServerAliveInterval=30 `
|
||||||
|
-o ServerAliveCountMax=3 `
|
||||||
|
-L "${LocalPort}:127.0.0.1:${RemoteMySqlPort}" `
|
||||||
|
-p $SshPort `
|
||||||
|
"${SshUser}@${RemoteHost}"
|
||||||
Reference in new issue
Block a user