Files
taiji-pda-v0/app/orchestration/page.tsx
T
zhanggangyong 127045e7b1 feat: 增加Agent评估按钮和工具集编排功能
- 数据与工具页面:在"使用的模型"右侧添加Agent评估按钮
- 编排中心:将"创建工作流"改为下拉菜单,支持"工具集"和"工作流"两种编排方式
- 工具集功能:支持选择最多8个工具组合成工具集
- API客户端:添加工具集相关接口(getToolsets, createToolset, deleteToolset)
2026-01-22 10:23:38 +00:00

646 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { AuthGuard } from "@/components/auth-guard"
import { DashboardLayout } from "@/components/dashboard-layout"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Network, Plus, Play, Save, Trash2, ArrowRight, Wrench, Workflow, ChevronDown } from "lucide-react"
import { useLanguage } from "@/hooks/useLanguage"
import { TaijiAPIClient } from "@/lib/api-client"
import { useToast } from "@/hooks/use-toast"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Label } from "@/components/ui/label"
import { Input } from "@/components/ui/input"
export default function OrchestrationPage() {
const { t } = useLanguage()
const { toast } = useToast()
const router = useRouter()
const [showCreateDialog, setShowCreateDialog] = useState(false)
const [showToolsetDialog, setShowToolsetDialog] = useState(false)
const [workflowNodes, setWorkflowNodes] = useState<string[]>([])
const [workflowName, setWorkflowName] = useState("")
const [selectedGateway, setSelectedGateway] = useState<string>("")
const [loading, setLoading] = useState(true)
const [availableAgents, setAvailableAgents] = useState<any[]>([])
const [workflows, setWorkflows] = useState<any[]>([])
// 工具集相关状态
const [toolsetName, setToolsetName] = useState("")
const [selectedTools, setSelectedTools] = useState<string[]>([])
const [availableTools, setAvailableTools] = useState<any[]>([])
const [toolsLoading, setToolsLoading] = useState(false)
const [toolsets, setToolsets] = useState<any[]>([])
useEffect(() => {
loadData()
}, [router])
const loadData = async () => {
try {
setLoading(true)
// 并行加载Agent列表、工作流列表和工具集列表
const [agentsResult, workflowsResult, toolsetsResult] = await Promise.allSettled([
TaijiAPIClient.getPlatformAgents(),
TaijiAPIClient.getWorkflows(),
TaijiAPIClient.getToolsets(),
])
// 加载平台Agent列表
if (agentsResult.status === "fulfilled" && agentsResult.value?.success) {
const data = agentsResult.value.data?.data || []
setAvailableAgents(data)
}
// 加载工作流列表
if (workflowsResult.status === "fulfilled" && workflowsResult.value?.success) {
const data = workflowsResult.value.data?.data || workflowsResult.value.data || []
setWorkflows(data)
}
// 加载工具集列表
if (toolsetsResult.status === "fulfilled" && toolsetsResult.value?.success) {
const data = toolsetsResult.value.data?.data || toolsetsResult.value.data || []
setToolsets(data)
}
} catch (error) {
console.error("Failed to load data:", error)
} finally {
setLoading(false)
}
}
// 加载可用工具列表(用于创建工具集)
const loadAvailableTools = async () => {
try {
setToolsLoading(true)
const result = await TaijiAPIClient.getUserTools()
if (result?.success && result.data?.tools) {
setAvailableTools(result.data.tools)
} else if (Array.isArray(result)) {
setAvailableTools(result)
}
} catch (error) {
console.error("Failed to load tools:", error)
} finally {
setToolsLoading(false)
}
}
// 打开工具集对话框
const openToolsetDialog = () => {
setShowToolsetDialog(true)
setToolsetName("")
setSelectedTools([])
loadAvailableTools()
}
// 切换工具选择
const toggleToolSelection = (toolId: string) => {
if (selectedTools.includes(toolId)) {
setSelectedTools(selectedTools.filter(id => id !== toolId))
} else if (selectedTools.length < 8) {
setSelectedTools([...selectedTools, toolId])
}
}
// 保存工具集
const handleSaveToolset = async () => {
if (!toolsetName || selectedTools.length === 0) {
return
}
try {
const result = await TaijiAPIClient.createToolset({
name: toolsetName,
tools: selectedTools,
})
if (result?.success) {
toast({
title: t("创建成功", "Success"),
description: t("工具集已创建", "Toolset created"),
})
setShowToolsetDialog(false)
setToolsetName("")
setSelectedTools([])
loadData()
} else {
throw new Error(result?.message || "Failed to create toolset")
}
} catch (error: any) {
toast({
title: t("创建失败", "Failed"),
description: error.message || t("无法创建工具集", "Failed to create toolset"),
variant: "destructive",
})
}
}
const addNode = (agentId: string) => {
if (workflowNodes.length < 3 && !workflowNodes.includes(agentId)) {
setWorkflowNodes([...workflowNodes, agentId])
}
}
const removeNode = (index: number) => {
setWorkflowNodes(workflowNodes.filter((_, i) => i !== index))
}
return (
<AuthGuard>
<DashboardLayout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">{t("编排中心", "Orchestration Hub")}</h1>
<p className="text-muted-foreground mt-1">
{t("创建和管理Agent工作流(最多3个节点)", "Create and manage agent workflows (max 3 nodes)")}
</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="gap-2">
<Plus className="h-4 w-4" />
{t("编排", "Orchestrate")}
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={openToolsetDialog} className="gap-2 cursor-pointer">
<Wrench className="h-4 w-4" />
{t("工具集", "Toolset")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setShowCreateDialog(true)} className="gap-2 cursor-pointer">
<Workflow className="h-4 w-4" />
{t("工作流", "Workflow")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground">
{t("总工作流", "Total Workflows")}
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{loading ? (
<span className="inline-block h-7 w-8 animate-pulse bg-muted rounded" />
) : (
workflows.length
)}
</div>
<p className="text-xs text-muted-foreground mt-1">{t("已创建", "Created")}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground">{t("运行中", "Running")}</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{loading ? (
<span className="inline-block h-7 w-4 animate-pulse bg-muted rounded" />
) : (
workflows.filter((w) => w.status === "running").length
)}
</div>
<p className="text-xs text-muted-foreground mt-1">{t("活跃工作流", "Active workflows")}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground">
{t("最大节点数", "Max Nodes")}
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">3</div>
<p className="text-xs text-muted-foreground mt-1">{t("每个工作流", "Per workflow")}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground">
{t("可用Agent", "Available Agents")}
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{loading ? (
<span className="inline-block h-7 w-8 animate-pulse bg-muted rounded" />
) : (
availableAgents.length
)}
</div>
<p className="text-xs text-muted-foreground mt-1">{t("平台和自定义", "Platform & Custom")}</p>
</CardContent>
</Card>
</div>
<div className="grid gap-4">
<Card>
<CardHeader>
<CardTitle>{t("我的工具集", "My Toolsets")}</CardTitle>
<CardDescription>{t("管理和执行Agent工作流", "Manage and execute agent workflows")}</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="space-y-4">
{[1, 2].map((i) => (
<div key={i} className="h-20 animate-pulse bg-muted rounded-lg" />
))}
</div>
) : workflows.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
{t("暂无工作流", "No workflows yet")}
</div>
) : (
<div className="space-y-4">
{workflows.map((workflow) => (
<div
key={workflow.id}
className="flex items-center justify-between rounded-lg border border-border bg-card p-4"
>
<div className="flex items-center gap-4 flex-1">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
<Network className="h-5 w-5 text-primary" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<h3 className="font-semibold">{workflow.name}</h3>
<Badge
className={
workflow.status === "running"
? "bg-green-500/10 text-green-500"
: "bg-gray-500/10 text-gray-500"
}
>
{workflow.status}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
{t("节点数", "Nodes")}: {workflow.nodes.length}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="gap-2 bg-transparent">
<Play className="h-4 w-4" />
{t("运行", "Run")}
</Button>
<Button variant="ghost" size="icon">
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
<div className="grid gap-4">
<Card>
<CardHeader>
<CardTitle>{t("工作流限制", "Workflow Limitations")}</CardTitle>
<CardDescription>{t("当前系统限制说明", "Current system limitations")}</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex items-center justify-between rounded-lg bg-muted/30 p-3">
<span className="text-sm font-medium">{t("最大Agent节点数", "Max Agent Nodes")}</span>
<Badge variant="outline">3</Badge>
</div>
<div className="flex items-center justify-between rounded-lg bg-muted/30 p-3">
<span className="text-sm font-medium">{t("支持的Agent类型", "Supported Agent Types")}</span>
<Badge variant="outline">{t("平台和自定义", "Platform & Custom")}</Badge>
</div>
<div className="flex items-center justify-between rounded-lg bg-muted/30 p-3">
<span className="text-sm font-medium">{t("并发执行", "Concurrent Execution")}</span>
<Badge variant="outline">{t("不支持", "Not Supported")}</Badge>
</div>
</div>
</CardContent>
</Card>
</div>
{/* 创建工作流对话框 */}
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>{t("创建新工作流", "Create New Workflow")}</DialogTitle>
<DialogDescription>
{t("选择最多3个Agent来构建工作流", "Select up to 3 agents to build your workflow")}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>{t("工作流名称", "Workflow Name")}</Label>
<Input
placeholder={t("输入工作流名称", "Enter workflow name")}
value={workflowName}
onChange={(e) => setWorkflowName(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>{t("选择服务网关", "Select Service Gateway")}</Label>
<div className="grid grid-cols-3 gap-3">
{[
{ id: "mcp", name: "MCP", desc: t("模型上下文协议", "Model Context Protocol") },
{ id: "a2a", name: "A2A", desc: t("Agent到Agent通信", "Agent-to-Agent Communication") },
{ id: "api", name: "API", desc: t("标准REST API", "Standard REST API") },
].map((gateway) => (
<button
key={gateway.id}
onClick={() => setSelectedGateway(gateway.id)}
className={`p-4 rounded-lg border text-left transition-all ${
selectedGateway === gateway.id
? "border-primary bg-primary/5 shadow-sm"
: "border-border hover:border-primary/50 hover:bg-muted/50"
}`}
>
<div className="font-semibold mb-1">{gateway.name}</div>
<div className="text-xs text-muted-foreground">{gateway.desc}</div>
</button>
))}
</div>
</div>
<div className="space-y-2">
<Label>{t("选择Agent(最多3个)", "Select Agents (max 3)")}</Label>
{availableAgents.length === 0 ? (
<div className="text-center py-8 text-muted-foreground border border-dashed rounded-lg">
{t("暂无可用Agent", "No agents available")}
</div>
) : (
<div className="grid grid-cols-2 gap-2 max-h-64 overflow-y-auto p-1">
{availableAgents.map((agent) => (
<button
key={agent.id}
onClick={() => addNode(agent.id)}
disabled={workflowNodes.length >= 3 || workflowNodes.includes(agent.id)}
className="flex items-center gap-3 p-3 rounded-lg border border-border hover:border-primary hover:bg-primary/5 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-left"
>
<span className="text-2xl">{agent.icon}</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{agent.name}</p>
{agent.custom && (
<Badge variant="secondary" className="text-xs mt-1">
{t("自定义", "Custom")}
</Badge>
)}
</div>
</button>
))}
</div>
)}
</div>
<div className="space-y-2">
<Label>{t("工作流结构", "Workflow Structure")}</Label>
<div className="rounded-lg border border-border bg-muted/30 p-4 min-h-[120px]">
{workflowNodes.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-8">
{t("请从上方选择Agent添加到工作流", "Select agents from above to add to workflow")}
</p>
) : (
<div className="flex items-center justify-center gap-2">
{workflowNodes.map((nodeId, index) => {
const agent = availableAgents.find((a) => a.id === nodeId)
return (
<div key={index} className="flex items-center gap-2">
<div className="relative group">
<div className="flex items-center gap-2 bg-primary/10 border border-primary/20 rounded-lg px-3 py-2">
<span className="text-xl">{agent?.icon}</span>
<span className="text-sm font-medium">{agent?.name}</span>
</div>
<button
onClick={() => removeNode(index)}
className="absolute -top-2 -right-2 h-5 w-5 rounded-full bg-destructive text-destructive-foreground opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center text-xs"
>
×
</button>
</div>
{index < workflowNodes.length - 1 && <ArrowRight className="h-5 w-5 text-primary" />}
</div>
)
})}
</div>
)}
</div>
<p className="text-xs text-muted-foreground">
{t("已选择", "Selected")}: {workflowNodes.length}/3
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowCreateDialog(false)}>
{t("取消", "Cancel")}
</Button>
<Button
onClick={async () => {
if (workflowNodes.length === 0 || !workflowName || !selectedGateway) {
return
}
try {
const result = await TaijiAPIClient.createWorkflow({
name: workflowName,
gateway: selectedGateway as "MCP" | "A2A" | "API",
nodes: workflowNodes.map((nodeId, index) => {
const agent = availableAgents.find((a) => a.id === nodeId)
return {
agentId: nodeId,
agentType: agent?.type === "custom" ? "custom" : "platform",
agentName: agent?.name || "",
order: index + 1,
}
}),
})
if (result?.success) {
toast({
title: t("创建成功", "Success"),
description: t("工作流已创建", "Workflow created"),
})
setShowCreateDialog(false)
setWorkflowNodes([])
setWorkflowName("")
setSelectedGateway("")
loadData()
} else {
throw new Error(result?.message || "Failed to create workflow")
}
} catch (error: any) {
toast({
title: t("创建失败", "Failed"),
description: error.message || t("无法创建工作流", "Failed to create workflow"),
variant: "destructive",
})
}
}}
disabled={workflowNodes.length === 0 || !workflowName || !selectedGateway}
>
<Save className="h-4 w-4 mr-2" />
{t("保存工作流", "Save Workflow")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 创建工具集对话框 */}
<Dialog open={showToolsetDialog} onOpenChange={setShowToolsetDialog}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>{t("创建工具集", "Create Toolset")}</DialogTitle>
<DialogDescription>
{t("选择最多8个工具来构建工具集", "Select up to 8 tools to build your toolset")}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>{t("工具集名称", "Toolset Name")}</Label>
<Input
placeholder={t("输入工具集名称", "Enter toolset name")}
value={toolsetName}
onChange={(e) => setToolsetName(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>{t("选择工具(最多8个)", "Select Tools (max 8)")}</Label>
{toolsLoading ? (
<div className="text-center py-8 text-muted-foreground border border-dashed rounded-lg">
{t("加载中...", "Loading...")}
</div>
) : availableTools.length === 0 ? (
<div className="text-center py-8 text-muted-foreground border border-dashed rounded-lg">
{t("暂无可用工具,请先在数据与工具页面创建工具", "No tools available, please create tools in Data & Tools page first")}
</div>
) : (
<div className="grid grid-cols-2 gap-3 max-h-80 overflow-y-auto p-1">
{availableTools.map((tool) => {
const isSelected = selectedTools.includes(tool.id || tool.name)
const isDisabled = !isSelected && selectedTools.length >= 8
return (
<div
key={tool.id || tool.name}
onClick={() => !isDisabled && toggleToolSelection(tool.id || tool.name)}
className={`flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-all ${
isSelected
? "border-primary bg-primary/5 shadow-sm"
: isDisabled
? "border-border opacity-50 cursor-not-allowed"
: "border-border hover:border-primary/50 hover:bg-muted/50"
}`}
>
<Checkbox
checked={isSelected}
disabled={isDisabled}
className="mt-1"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<Wrench className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium truncate">{tool.name}</p>
</div>
{tool.description && (
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
{tool.description}
</p>
)}
{tool.template && (
<Badge variant="secondary" className="text-xs mt-2">
{tool.template}
</Badge>
)}
</div>
</div>
)
})}
</div>
)}
<p className="text-xs text-muted-foreground">
{t("已选择", "Selected")}: {selectedTools.length}/8
</p>
</div>
{selectedTools.length > 0 && (
<div className="space-y-2">
<Label>{t("已选工具", "Selected Tools")}</Label>
<div className="rounded-lg border border-border bg-muted/30 p-4">
<div className="flex flex-wrap gap-2">
{selectedTools.map((toolId) => {
const tool = availableTools.find((t) => (t.id || t.name) === toolId)
return (
<div
key={toolId}
className="flex items-center gap-2 bg-primary/10 border border-primary/20 rounded-lg px-3 py-1.5 group"
>
<Wrench className="h-3 w-3 text-primary" />
<span className="text-sm font-medium">{tool?.name || toolId}</span>
<button
onClick={() => toggleToolSelection(toolId)}
className="h-4 w-4 rounded-full bg-destructive/80 text-destructive-foreground opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center text-xs hover:bg-destructive"
>
×
</button>
</div>
)
})}
</div>
</div>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowToolsetDialog(false)}>
{t("取消", "Cancel")}
</Button>
<Button
onClick={handleSaveToolset}
disabled={selectedTools.length === 0 || !toolsetName}
>
<Save className="h-4 w-4 mr-2" />
{t("保存工具集", "Save Toolset")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</DashboardLayout>
</AuthGuard>
)
}