diff --git a/docs b/docs index abf6a84..1dee786 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit abf6a84eba91296553f35ff4c22336152143a533 +Subproject commit 1dee786c06abb2f524441458f0f685bc1f5ae04e diff --git a/web/app/dashboard/ai-workflows/_components/workflow-workbench.tsx b/web/app/dashboard/ai-workflows/_components/workflow-workbench.tsx new file mode 100644 index 0000000..8ecacae --- /dev/null +++ b/web/app/dashboard/ai-workflows/_components/workflow-workbench.tsx @@ -0,0 +1,35 @@ +"use client" + +import { useCallback, useEffect, useState } from "react" +import { ArrowLeftIcon } from "lucide-react" +import { useRouter } from "next/navigation" +import { toast } from "sonner" + +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Textarea } from "@/components/ui/textarea" +import { formatDateTime } from "@/lib/utils" +import { createAIWorkflow, fetchAIWorkflow, fetchAIWorkflowNodeSpecs, fetchAIWorkflowUsage, fetchAIWorkflowVersions, publishAIWorkflow, restoreAIWorkflowVersion, updateAIWorkflow, validateAIWorkflow, type AIWorkflow, type AIWorkflowDefinition, type AIWorkflowNodeSpec, type AIWorkflowUsage, type AIWorkflowVersion } from "@/lib/api/admin" + +import { WorkflowEditor } from "./workflow-editor" + +const emptyDefinition: AIWorkflowDefinition = { schemaVersion: 2, nodes: [{ id: "start_1", type: "start", meta: { position: { x: 0, y: 80 } }, data: { title: "开始", config: {}, inputsValues: {} } }, { id: "end_1", type: "end", meta: { position: { x: 260, y: 80 } }, data: { title: "结束", config: {}, inputsValues: {} } }], edges: [{ sourceNodeID: "start_1", targetNodeID: "end_1", sourcePortID: "edge_start_end" }] } + +export function WorkflowWorkbench({ workflowID, onClose, onSaved }: { workflowID?: number; onClose?: () => void; onSaved?: () => void }) { + const router = useRouter() + const [active, setActive] = useState(null); const [nodeSpecs, setNodeSpecs] = useState([]) + const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [definition, setDefinition] = useState(emptyDefinition) + const [versions, setVersions] = useState([]); const [usage, setUsage] = useState([]); const [saving, setSaving] = useState(false); const [dirty, setDirty] = useState(false) + const [metadataOpen, setMetadataOpen] = useState(false); const [metadataName, setMetadataName] = useState(""); const [metadataDescription, setMetadataDescription] = useState("") + const load = useCallback(async () => { const specs = await fetchAIWorkflowNodeSpecs(); setNodeSpecs(specs ?? []); if (!workflowID) return; const [item, versionPage, uses] = await Promise.all([fetchAIWorkflow(workflowID), fetchAIWorkflowVersions({ workflowId: workflowID, limit: 50 }), fetchAIWorkflowUsage(workflowID)]); setActive(item); setName(item.name); setDescription(item.description); setDefinition(item.draftDefinition); setVersions(versionPage.results ?? []); setUsage(uses ?? []); setDirty(false) }, [workflowID]) + useEffect(() => { void load().catch((error) => toast.error(error instanceof Error ? error.message : "加载工作流失败")) }, [load]) + const openMetadata = () => { setMetadataName(name); setMetadataDescription(description); setMetadataOpen(true) } + const applyMetadata = () => { setName(metadataName); setDescription(metadataDescription); setDirty(true); setMetadataOpen(false) } + async function save() { if (!name.trim()) return toast.error("请填写工作流名称"); setSaving(true); try { if (active) { await updateAIWorkflow({ id: active.id, name: name.trim(), description: description.trim(), definition }); await load() } else { const created = await createAIWorkflow({ name: name.trim(), description: description.trim(), definition }); setActive(created); setName(created.name); setDescription(created.description); setDefinition(created.draftDefinition) }; onSaved?.(); toast.success("草稿已保存") } catch (error) { toast.error(error instanceof Error ? error.message : "保存失败") } finally { setSaving(false) } } + async function publish() { if (!active) return toast.error("请先保存草稿"); setSaving(true); try { const version = await publishAIWorkflow(active.id, definition); await load(); toast.success(`已发布 v${version.version}`) } catch (error) { toast.error(error instanceof Error ? error.message : "发布失败") } finally { setSaving(false) } } + async function restore(version: AIWorkflowVersion) { if (!active) return; try { await restoreAIWorkflowVersion(active.id, version.id); await load(); toast.success(`已将 v${version.version} 恢复为草稿`) } catch (error) { toast.error(error instanceof Error ? error.message : "恢复失败") } } + return

{name || "新建工作流"}

{active?.publishedVersionId ? "已发布" : "草稿"}{dirty ? 未保存 : null}

{description || "暂未填写业务说明"}

编辑画布版本历史 ({versions.length})使用情况 ({usage.length})
{ setDefinition(next); setDirty(true) }} onSaveDraft={() => void save()} onPublish={() => void publish()} saveDraftDisabled={saving} publishDisabled={saving || !active} />{versions.length ? versions.map((version) =>
v{version.version}
发布于 {formatDateTime(version.publishedAt || version.createdAt)}
发布人:{version.publishedByName || "-"}
) :

尚未发布版本。

}
{usage.length ? usage.map((item) =>
{item.aiAgentName}
固定关联 v{item.workflowVersion}
{item.enabled ? "启用" : "已停用"}
) :

暂未被任何 Agent 使用。

}
编辑工作流信息
setMetadataName(event.target.value)} placeholder="工作流名称" />