forked from xiaohei/taiji-pda-v0
- 更新 createTool 接口:使用 template + envConfig 格式替代 type + config - 更新 createCustomAgent 接口:添加 tools 参数支持,从工具获取配置 - 更新 updateTool 接口:支持 is_active 和 envConfig 参数 - 工具列表展示 template、description、is_active 字段 - 部署Agent时从已创建的工具列表中选择 - 环境变量配置过滤 OPENAI_API_KEY(系统自动注入)
1436 lines
63 KiB
TypeScript
1436 lines
63 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect } from "react"
|
||
import { useRouter } from "next/navigation"
|
||
import { useLanguage } from "@/hooks/useLanguage"
|
||
import { TaijiAPIClient } from "@/lib/api-client"
|
||
import { AuthGuard } from "@/components/auth-guard"
|
||
import { DashboardLayout } from "@/components/dashboard-layout"
|
||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Label } from "@/components/ui/label"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
import { Plus, Search, Trash2, FileJson, Cloud, Cpu, Code2, Boxes, Workflow, Upload, RefreshCw, Square, Play, Bot } from "lucide-react"
|
||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog"
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||
import { Checkbox } from "@/components/ui/checkbox"
|
||
|
||
export default function DataToolsPage() {
|
||
const { t } = useLanguage()
|
||
const router = useRouter()
|
||
const [tools, setTools] = useState<any[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [stats, setStats] = useState({ totalApis: 0, generatedTools: 0, activePods: 0 })
|
||
|
||
const [showNewToolDialog, setShowNewToolDialog] = useState(false)
|
||
const [showTemplateDialog, setShowTemplateDialog] = useState(false)
|
||
const [templateType, setTemplateType] = useState<"json_api" | "cloud_storage" | "database">("json_api")
|
||
const [useJsonUpload, setUseJsonUpload] = useState(false)
|
||
const [templateName, setTemplateName] = useState("")
|
||
const [templateConfig, setTemplateConfig] = useState({
|
||
// JSON API 配置
|
||
url: "",
|
||
apiKey: "",
|
||
queryParams: "",
|
||
jsonContent: "",
|
||
// 云存储/数据库配置
|
||
service: "",
|
||
dbType: "",
|
||
connectionString: "",
|
||
})
|
||
const [creatingTemplate, setCreatingTemplate] = useState(false)
|
||
const [podConfig, setPodConfig] = useState({
|
||
agentCount: 1,
|
||
cpuCores: 1,
|
||
memoryGB: 2,
|
||
maxAgents: 5,
|
||
model: "gpt-4o-mini",
|
||
})
|
||
const [selectedFramework, setSelectedFramework] = useState<string>("")
|
||
const [selectedServiceGateway, setSelectedServiceGateway] = useState<string>("")
|
||
const [selectedDataTemplate, setSelectedDataTemplate] = useState<string>("")
|
||
const [dataTemplates, setDataTemplates] = useState<any[]>([])
|
||
const [frameworkTemplates, setFrameworkTemplates] = useState<any[]>([])
|
||
// 新增:API返回的数据存储模块(含环境变量信息)
|
||
const [apiDataTemplates, setApiDataTemplates] = useState<any[]>([])
|
||
const [selectedApiDataTemplate, setSelectedApiDataTemplate] = useState<string>("")
|
||
const [envConfig, setEnvConfig] = useState<Record<string, string>>({})
|
||
// A2A 框架专用字段
|
||
const [agentRole, setAgentRole] = useState<string>("")
|
||
const [agentCapabilities, setAgentCapabilities] = useState<string>("")
|
||
const [availableModels, setAvailableModels] = useState<any[]>([])
|
||
const [toolName, setToolName] = useState<string>("")
|
||
const [deploying, setDeploying] = useState(false)
|
||
|
||
// 自定义Agent管理状态
|
||
const [customAgents, setCustomAgents] = useState<any[]>([])
|
||
const [customAgentsLoading, setCustomAgentsLoading] = useState(true)
|
||
const [actionLoading, setActionLoading] = useState<string | null>(null)
|
||
|
||
useEffect(() => {
|
||
loadTools()
|
||
loadStats()
|
||
loadCustomAgents()
|
||
loadFrameworkTemplates()
|
||
loadModels()
|
||
}, [])
|
||
|
||
// 加载框架模块列表
|
||
const loadFrameworkTemplates = async () => {
|
||
try {
|
||
const result = await TaijiAPIClient.getCustomAgentTemplates()
|
||
if (result?.success && result.data) {
|
||
// 新格式:{ frameworkTemplates: [...], dataTemplates: [...] }
|
||
if (result.data.frameworkTemplates) {
|
||
// 框架模块:字符串数组 ["A2A", "langchain", "MCP"]
|
||
const templates = result.data.frameworkTemplates.map((template: string) => {
|
||
const displayNames: Record<string, string> = {
|
||
'A2A': 'A2A框架',
|
||
'langchain': 'LangChain框架',
|
||
'MCP': 'MCP框架'
|
||
}
|
||
return {
|
||
name: template,
|
||
displayName: displayNames[template] || template,
|
||
description: ''
|
||
}
|
||
})
|
||
setFrameworkTemplates(templates)
|
||
}
|
||
|
||
// 数据存储模块:对象数组,包含 env_info
|
||
if (result.data.dataTemplates) {
|
||
setApiDataTemplates(result.data.dataTemplates)
|
||
}
|
||
|
||
// 兼容旧格式:{ templates: [...] }
|
||
if (result.data.templates && !result.data.frameworkTemplates) {
|
||
const templates = result.data.templates.map((template: string | any) => {
|
||
if (typeof template === 'string') {
|
||
const displayNames: Record<string, string> = {
|
||
'A2A': 'A2A框架',
|
||
'langchain': 'LangChain框架',
|
||
'MCP': 'MCP框架'
|
||
}
|
||
return {
|
||
name: template,
|
||
displayName: displayNames[template] || template,
|
||
description: ''
|
||
}
|
||
}
|
||
return template
|
||
})
|
||
setFrameworkTemplates(templates)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to load framework templates:", error)
|
||
}
|
||
}
|
||
|
||
// 加载模型列表
|
||
const loadModels = async () => {
|
||
try {
|
||
const result = await TaijiAPIClient.getUserModels()
|
||
if (result?.success && result.data?.models) {
|
||
setAvailableModels(result.data.models)
|
||
// 如果当前选择的模型不在列表中,使用第一个模型作为默认值
|
||
if (result.data.models.length > 0 && !result.data.models.find((m: any) => m.id === podConfig.model)) {
|
||
setPodConfig({ ...podConfig, model: result.data.models[0].id })
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to load models:", error)
|
||
}
|
||
}
|
||
|
||
const loadTools = async () => {
|
||
try {
|
||
setLoading(true)
|
||
// 使用文档规定的接口:GET /api/user/tools
|
||
const result = await TaijiAPIClient.getUserTools()
|
||
if (result?.success && result.data?.tools) {
|
||
setTools(result.data.tools)
|
||
// 将已注册的工具作为数据模块选项
|
||
setDataTemplates(result.data.tools)
|
||
} else if (Array.isArray(result)) {
|
||
// 兼容旧格式
|
||
setTools(result)
|
||
setDataTemplates(result)
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to load tools:", error)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const loadStats = async () => {
|
||
try {
|
||
// 使用文档规定的接口:GET /api/user/tools/stats
|
||
const result = await TaijiAPIClient.getToolsStats()
|
||
if (result?.success && result.data) {
|
||
setStats({
|
||
totalApis: result.data.totalTools || 0,
|
||
generatedTools: result.data.generatedTools || 0,
|
||
activePods: result.data.activeTools || 0,
|
||
})
|
||
} else if (result) {
|
||
// 兼容旧格式(Data Ingestion Service)
|
||
setStats({
|
||
totalApis: result.total_apis || 0,
|
||
generatedTools: result.generated_tools || 0,
|
||
activePods: result.active_pods || 0,
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to load stats:", error)
|
||
}
|
||
}
|
||
|
||
// 加载自定义Agent列表
|
||
const loadCustomAgents = async () => {
|
||
try {
|
||
setCustomAgentsLoading(true)
|
||
const result = await TaijiAPIClient.getUserCustomAgents()
|
||
if (result?.success && result.data?.agents) {
|
||
setCustomAgents(result.data.agents)
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to load custom agents:", error)
|
||
} finally {
|
||
setCustomAgentsLoading(false)
|
||
}
|
||
}
|
||
|
||
// 删除自定义Agent
|
||
const handleDeleteCustomAgent = async (agentName: string) => {
|
||
if (!confirm(t(`确定要删除Agent "${agentName}" 吗?此操作不可恢复。`, `Are you sure you want to delete agent "${agentName}"? This action cannot be undone.`))) {
|
||
return
|
||
}
|
||
try {
|
||
setActionLoading(agentName)
|
||
const result = await TaijiAPIClient.deleteCustomAgent(agentName)
|
||
if (result?.success) {
|
||
loadCustomAgents()
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to delete custom agent:", error)
|
||
} finally {
|
||
setActionLoading(null)
|
||
}
|
||
}
|
||
|
||
// 停止自定义Agent
|
||
const handleStopCustomAgent = async (agentName: string) => {
|
||
if (!confirm(t(`确定要停止Agent "${agentName}" 吗?`, `Are you sure you want to stop agent "${agentName}"?`))) {
|
||
return
|
||
}
|
||
try {
|
||
setActionLoading(agentName)
|
||
const result = await TaijiAPIClient.stopCustomAgent(agentName)
|
||
if (result?.success) {
|
||
loadCustomAgents()
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to stop custom agent:", error)
|
||
} finally {
|
||
setActionLoading(null)
|
||
}
|
||
}
|
||
|
||
// 重启自定义Agent
|
||
const handleRestartCustomAgent = async (agentName: string) => {
|
||
try {
|
||
setActionLoading(agentName)
|
||
const result = await TaijiAPIClient.restartCustomAgent(agentName)
|
||
if (result?.success) {
|
||
loadCustomAgents()
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to restart custom agent:", error)
|
||
} finally {
|
||
setActionLoading(null)
|
||
}
|
||
}
|
||
|
||
// 删除工具
|
||
const handleDeleteTool = async (toolId: string) => {
|
||
if (!confirm(t(`确定要删除工具吗?此操作不可恢复。`, `Are you sure you want to delete this tool? This action cannot be undone.`))) {
|
||
return
|
||
}
|
||
try {
|
||
const result = await TaijiAPIClient.deleteUserTool(toolId)
|
||
if (result?.success) {
|
||
alert(t("工具删除成功", "Tool deleted successfully"))
|
||
loadTools()
|
||
loadStats()
|
||
} else {
|
||
throw new Error(result?.message || "Delete failed")
|
||
}
|
||
} catch (error: any) {
|
||
console.error("Failed to delete tool:", error)
|
||
alert(t("删除失败", "Delete failed") + ": " + (error.message || t("未知错误", "Unknown error")))
|
||
}
|
||
}
|
||
|
||
// 创建数据模块(实际调用创建工具接口)
|
||
// 根据接口文档 v4:使用 template + envConfig 格式
|
||
const handleCreateTemplate = async () => {
|
||
if (!templateName) {
|
||
alert(t("请输入模块名称", "Please enter module name"))
|
||
return
|
||
}
|
||
|
||
try {
|
||
setCreatingTemplate(true)
|
||
|
||
let description = ""
|
||
let template = ""
|
||
let toolEnvConfig: Record<string, string> = {}
|
||
|
||
// 数据存储模块(来自API的dataTemplates)
|
||
if (templateType === "data_storage" as any) {
|
||
if (!selectedApiDataTemplate) {
|
||
alert(t("请选择数据存储模块", "Please select a data storage module"))
|
||
return
|
||
}
|
||
|
||
const templateInfo = apiDataTemplates.find(t => t.template === selectedApiDataTemplate)
|
||
if (!templateInfo) {
|
||
alert(t("无效的模块", "Invalid module"))
|
||
return
|
||
}
|
||
|
||
// 校验必填环境变量(排除 OPENAI_API_KEY,系统会自动注入)
|
||
const required = templateInfo.env_info?.required || {}
|
||
for (const key of Object.keys(required)) {
|
||
// OPENAI_API_KEY 无需用户填写,系统自动注入
|
||
if (key === "OPENAI_API_KEY") continue
|
||
if (!envConfig[key]) {
|
||
alert(t(`请填写 ${key}`, `Please fill in ${key}`))
|
||
return
|
||
}
|
||
}
|
||
|
||
template = selectedApiDataTemplate
|
||
toolEnvConfig = { ...envConfig }
|
||
description = templateInfo.description || t("数据存储工具", "Data Storage Tool")
|
||
} else if (templateType === "json_api") {
|
||
// JSON API 模块 - 暂不支持新格式,保留原逻辑提示
|
||
alert(t("JSON API模块即将支持,请使用数据存储模块", "JSON API module coming soon, please use data storage module"))
|
||
return
|
||
} else if (templateType === "cloud_storage") {
|
||
// 云存储模块 - 暂不支持新格式,保留原逻辑提示
|
||
alert(t("云存储模块即将支持,请使用数据存储模块", "Cloud storage module coming soon, please use data storage module"))
|
||
return
|
||
}
|
||
|
||
// 使用创建工具接口 POST /api/user/tools/create
|
||
// 根据接口文档 v4 格式:name, description, template, envConfig
|
||
const result = await TaijiAPIClient.createTool({
|
||
name: templateName,
|
||
description: description || t("数据模块工具", "Data Module Tool"),
|
||
template: template,
|
||
envConfig: toolEnvConfig,
|
||
})
|
||
|
||
if (result?.success) {
|
||
alert(t("工具创建成功", "Tool created successfully"))
|
||
setShowTemplateDialog(false)
|
||
// 重置表单
|
||
setTemplateName("")
|
||
setTemplateConfig({
|
||
url: "",
|
||
apiKey: "",
|
||
queryParams: "",
|
||
jsonContent: "",
|
||
service: "",
|
||
dbType: "",
|
||
connectionString: "",
|
||
})
|
||
setUseJsonUpload(false)
|
||
setSelectedApiDataTemplate("")
|
||
setEnvConfig({})
|
||
// 刷新工具列表和统计数据
|
||
await loadTools()
|
||
await loadStats()
|
||
} else {
|
||
throw new Error(result?.message || "Create tool failed")
|
||
}
|
||
} catch (error: any) {
|
||
console.error("Failed to create tool:", error)
|
||
alert(t("创建失败", "Create failed") + ": " + (error.message || t("未知错误", "Unknown error")))
|
||
} finally {
|
||
setCreatingTemplate(false)
|
||
}
|
||
}
|
||
|
||
// 部署Agent(创建自定义Agent)
|
||
// 根据接口文档 v4:使用 tools 参数,系统自动从工具获取 template 和 envConfig
|
||
const handleDeployAgent = async () => {
|
||
// 校验必填字段:Agent名称必填,工具或数据存储模块二选一
|
||
if (!toolName) {
|
||
alert(t("请填写Agent名称", "Please enter agent name"))
|
||
return
|
||
}
|
||
|
||
// 获取选中的工具(从已创建的工具列表中选择)
|
||
const selectedToolIds = selectedDataTemplate ? [selectedDataTemplate] : []
|
||
|
||
// 如果没有选择已有工具,但选择了数据存储模块,需要提示先创建工具
|
||
if (selectedToolIds.length === 0 && selectedApiDataTemplate) {
|
||
// 如果选了模板但没选工具,说明要直接用模板创建(旧模式)
|
||
// 新接口要求必须先创建工具,再基于工具创建Agent
|
||
alert(t("请先在「工具模块」标签页创建工具,然后再部署Agent", "Please create a tool first in the 'Tool Modules' tab, then deploy the Agent"))
|
||
return
|
||
}
|
||
|
||
if (selectedToolIds.length === 0) {
|
||
alert(t("请选择一个已创建的工具", "Please select a created tool"))
|
||
return
|
||
}
|
||
|
||
try {
|
||
setDeploying(true)
|
||
|
||
// 将CPU和内存转换为K8s格式
|
||
const cpuRequest = `${podConfig.cpuCores * 1000}m` // 转换为millicores
|
||
const cpuLimit = `${podConfig.cpuCores * 1000}m` // 默认与request相同
|
||
const memoryRequest = `${podConfig.memoryGB}Gi`
|
||
const memoryLimit = `${podConfig.memoryGB}Gi` // 默认与request相同
|
||
|
||
// 框架类型(默认MCP)
|
||
const frameworkName = selectedFramework || "MCP"
|
||
|
||
// 构建请求参数 - 根据接口文档 v4 格式
|
||
const requestData: any = {
|
||
name: toolName, // 必填
|
||
tools: selectedToolIds, // 必填:工具ID列表
|
||
frameworkTemplate: frameworkName, // 可选:框架类型(A2A/langchain/MCP)
|
||
description: toolName, // 可选:Agent描述
|
||
cpuRequest: cpuRequest, // 可选:CPU请求量,默认 "100m"
|
||
cpuLimit: cpuLimit, // 可选:CPU限制量
|
||
memoryRequest: memoryRequest, // 可选:内存请求量,默认 "128Mi"
|
||
memoryLimit: memoryLimit, // 可选:内存限制量
|
||
model: podConfig.model, // 可选:模型名称
|
||
}
|
||
|
||
// 添加额外环境变量配置(与工具配置合并,请求中的优先)
|
||
if (Object.keys(envConfig).length > 0) {
|
||
requestData.envConfig = envConfig
|
||
}
|
||
|
||
// A2A 框架专用字段
|
||
if (frameworkName === "A2A") {
|
||
if (agentRole) {
|
||
requestData.agentRole = agentRole
|
||
}
|
||
if (agentCapabilities) {
|
||
// 将逗号分隔的字符串转换为数组
|
||
requestData.agentCapabilities = agentCapabilities.split(",").map(s => s.trim()).filter(Boolean)
|
||
}
|
||
}
|
||
|
||
const result = await TaijiAPIClient.createCustomAgent(requestData)
|
||
|
||
if (result?.success) {
|
||
alert(t("自定义Agent创建成功,正在部署", "Custom Agent created successfully, deploying"))
|
||
setShowNewToolDialog(false)
|
||
// 重置表单
|
||
setSelectedFramework("")
|
||
setSelectedServiceGateway("")
|
||
setSelectedDataTemplate("")
|
||
setSelectedApiDataTemplate("")
|
||
setEnvConfig({})
|
||
setAgentRole("")
|
||
setAgentCapabilities("")
|
||
setToolName("")
|
||
setPodConfig({
|
||
agentCount: 1,
|
||
cpuCores: 1,
|
||
memoryGB: 2,
|
||
maxAgents: 5,
|
||
model: "gpt-4o-mini",
|
||
})
|
||
// 刷新数据
|
||
loadCustomAgents()
|
||
loadStats()
|
||
} else {
|
||
throw new Error(result?.message || "Deployment failed")
|
||
}
|
||
} catch (error: any) {
|
||
console.error("Failed to deploy agent:", error)
|
||
alert(t("部署失败", "Deployment failed") + ": " + (error.message || t("未知错误", "Unknown error")))
|
||
} finally {
|
||
setDeploying(false)
|
||
}
|
||
}
|
||
|
||
// 获取状态颜色
|
||
const getStatusColor = (status: string) => {
|
||
switch (status?.toLowerCase()) {
|
||
case "running":
|
||
return "bg-green-500/10 text-green-500"
|
||
case "stopped":
|
||
return "bg-gray-500/10 text-gray-500"
|
||
case "pending":
|
||
return "bg-yellow-500/10 text-yellow-500"
|
||
case "failed":
|
||
return "bg-red-500/10 text-red-500"
|
||
default:
|
||
return "bg-gray-500/10 text-gray-500"
|
||
}
|
||
}
|
||
|
||
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("数据与工具", "Data & Tools")}</h1>
|
||
<p className="text-muted-foreground mt-1">
|
||
{t("API市场和工具生成平台", "API marketplace and tool generation platform")}
|
||
</p>
|
||
</div>
|
||
<Button onClick={() => setShowNewToolDialog(true)} className="gap-2">
|
||
<Plus className="h-4 w-4" />
|
||
{t("部署Agent", "Deploy Agent")}
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="grid gap-4 md:grid-cols-3">
|
||
<Card>
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-sm font-medium text-muted-foreground">{t("总工具数", "Total Tools")}</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="text-2xl font-bold">
|
||
{loading ? (
|
||
<span className="inline-block h-7 w-12 animate-pulse bg-muted rounded" />
|
||
) : (
|
||
stats.totalApis
|
||
)}
|
||
</div>
|
||
<p className="text-xs text-muted-foreground mt-1">
|
||
{loading ? "" : `${stats.totalApis - stats.generatedTools} ${t("个已处理", "processed")}`}
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||
{t("生成的工具", "Generated Tools")}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="text-2xl font-bold">
|
||
{loading ? (
|
||
<span className="inline-block h-7 w-8 animate-pulse bg-muted rounded" />
|
||
) : (
|
||
stats.generatedTools
|
||
)}
|
||
</div>
|
||
<p className="text-xs text-muted-foreground mt-1">{t("可以使用", "Ready to use")}</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||
{t("活跃的工具数", "Active Tools")}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="text-2xl font-bold">
|
||
{loading ? (
|
||
<span className="inline-block h-7 w-8 animate-pulse bg-muted rounded" />
|
||
) : (
|
||
stats.activePods
|
||
)}
|
||
</div>
|
||
<p className="text-xs text-muted-foreground mt-1">{t("运行中", "Running")}</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
<Tabs defaultValue="tools" className="space-y-4">
|
||
<TabsList>
|
||
<TabsTrigger value="tools">{t("可用工具", "Available Tools")}</TabsTrigger>
|
||
<TabsTrigger value="templates">{t("工具模块", "Tool Modules")}</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="tools" className="space-y-4">
|
||
<Card>
|
||
<CardHeader>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<CardTitle>{t("工具注册表", "Tool Registry")}</CardTitle>
|
||
<CardDescription>
|
||
{t("管理和监控生成的API工具", "Manage and monitor generated API tools")}
|
||
</CardDescription>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<Input placeholder={t("搜索工具...", "Search tools...")} className="w-64" />
|
||
<Button variant="outline" size="icon">
|
||
<Search className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>{t("工具名称", "Tool Name")}</TableHead>
|
||
<TableHead>{t("描述", "Description")}</TableHead>
|
||
<TableHead>{t("模板类型", "Template")}</TableHead>
|
||
<TableHead>{t("分类", "Category")}</TableHead>
|
||
<TableHead>{t("状态", "Status")}</TableHead>
|
||
<TableHead>{t("创建时间", "Created")}</TableHead>
|
||
<TableHead className="text-right">{t("操作", "Actions")}</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{loading ? (
|
||
<TableRow>
|
||
<TableCell colSpan={7} className="text-center py-8">
|
||
<span className="text-muted-foreground">{t("加载中...", "Loading...")}</span>
|
||
</TableCell>
|
||
</TableRow>
|
||
) : tools.length === 0 ? (
|
||
<TableRow>
|
||
<TableCell colSpan={7} className="text-center py-8">
|
||
<span className="text-muted-foreground">{t("暂无工具", "No tools available")}</span>
|
||
</TableCell>
|
||
</TableRow>
|
||
) : (
|
||
tools.map((tool) => (
|
||
<TableRow key={tool.id || tool.name}>
|
||
<TableCell className="font-medium">{tool.name}</TableCell>
|
||
<TableCell className="text-sm text-muted-foreground max-w-[200px] truncate">
|
||
{tool.description || "-"}
|
||
</TableCell>
|
||
<TableCell>
|
||
<Badge variant="outline">{tool.template || "-"}</Badge>
|
||
</TableCell>
|
||
<TableCell>
|
||
<Badge variant="secondary">{tool.category || tool.type || "database"}</Badge>
|
||
</TableCell>
|
||
<TableCell>
|
||
<Badge className={tool.is_active !== false ? "bg-green-500/10 text-green-500" : "bg-gray-500/10 text-gray-500"}>
|
||
{tool.is_active !== false ? t("激活", "Active") : t("停用", "Inactive")}
|
||
</Badge>
|
||
</TableCell>
|
||
<TableCell className="text-sm text-muted-foreground">
|
||
{tool.created_at ? new Date(tool.created_at).toLocaleString() : "-"}
|
||
</TableCell>
|
||
<TableCell className="text-right">
|
||
<div className="flex justify-end gap-2">
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
title={t("删除工具", "Delete Tool")}
|
||
onClick={() => handleDeleteTool(tool.id || tool.name)}
|
||
>
|
||
<Trash2 className="h-4 w-4 text-destructive" />
|
||
</Button>
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
))
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 自定义Agent管理 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Bot className="h-5 w-5" />
|
||
{t("自定义Agent管理", "Custom Agent Management")}
|
||
</CardTitle>
|
||
<CardDescription>
|
||
{t("管理已部署的自定义Agent,支持停止、重启和删除操作", "Manage deployed custom agents with stop, restart and delete operations")}
|
||
</CardDescription>
|
||
</div>
|
||
<Button variant="outline" size="sm" onClick={loadCustomAgents} className="gap-2">
|
||
<RefreshCw className="h-4 w-4" />
|
||
{t("刷新", "Refresh")}
|
||
</Button>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>{t("Agent名称", "Agent Name")}</TableHead>
|
||
<TableHead>{t("模块", "Module")}</TableHead>
|
||
<TableHead>{t("状态", "Status")}</TableHead>
|
||
<TableHead>{t("CPU", "CPU")}</TableHead>
|
||
<TableHead>{t("内存", "Memory")}</TableHead>
|
||
<TableHead>{t("运行时间", "Runtime")}</TableHead>
|
||
<TableHead className="text-right">{t("操作", "Actions")}</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{customAgentsLoading ? (
|
||
<TableRow>
|
||
<TableCell colSpan={7} className="text-center py-8">
|
||
<span className="text-muted-foreground">{t("加载中...", "Loading...")}</span>
|
||
</TableCell>
|
||
</TableRow>
|
||
) : customAgents.length === 0 ? (
|
||
<TableRow>
|
||
<TableCell colSpan={7} className="text-center py-8">
|
||
<span className="text-muted-foreground">{t("暂无自定义Agent", "No custom agents")}</span>
|
||
</TableCell>
|
||
</TableRow>
|
||
) : (
|
||
customAgents.map((agent) => (
|
||
<TableRow key={agent.name}>
|
||
<TableCell className="font-medium">{agent.name}</TableCell>
|
||
<TableCell>
|
||
<Badge variant="secondary">{agent.template || "-"}</Badge>
|
||
</TableCell>
|
||
<TableCell>
|
||
<Badge className={getStatusColor(agent.status)}>
|
||
{agent.status || "Unknown"}
|
||
</Badge>
|
||
</TableCell>
|
||
<TableCell className="text-sm">{agent.cpu || "-"}</TableCell>
|
||
<TableCell className="text-sm">{agent.memory || "-"}</TableCell>
|
||
<TableCell className="text-sm text-muted-foreground">
|
||
{agent.runningSeconds
|
||
? `${Math.floor(agent.runningSeconds / 3600)}h ${Math.floor((agent.runningSeconds % 3600) / 60)}m`
|
||
: "-"
|
||
}
|
||
</TableCell>
|
||
<TableCell className="text-right">
|
||
<div className="flex justify-end gap-1">
|
||
{agent.status?.toLowerCase() === "running" ? (
|
||
<>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
title={t("停止", "Stop")}
|
||
disabled={actionLoading === agent.name}
|
||
onClick={() => handleStopCustomAgent(agent.name)}
|
||
>
|
||
<Square className="h-4 w-4 text-yellow-500" />
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
title={t("重启", "Restart")}
|
||
disabled={actionLoading === agent.name}
|
||
onClick={() => handleRestartCustomAgent(agent.name)}
|
||
>
|
||
<RefreshCw className="h-4 w-4 text-blue-500" />
|
||
</Button>
|
||
</>
|
||
) : (
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
title={t("启动", "Start")}
|
||
disabled={actionLoading === agent.name}
|
||
onClick={() => handleRestartCustomAgent(agent.name)}
|
||
>
|
||
<Play className="h-4 w-4 text-green-500" />
|
||
</Button>
|
||
)}
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
title={t("删除", "Delete")}
|
||
disabled={actionLoading === agent.name}
|
||
onClick={() => handleDeleteCustomAgent(agent.name)}
|
||
>
|
||
<Trash2 className="h-4 w-4 text-destructive" />
|
||
</Button>
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
))
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="templates" className="space-y-4">
|
||
<Card>
|
||
<CardHeader>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<CardTitle>{t("数据模块管理", "Data Module Management")}</CardTitle>
|
||
<CardDescription>
|
||
{t("配置JSON接口和云存储数据源", "Configure JSON interfaces and cloud storage data sources")}
|
||
</CardDescription>
|
||
</div>
|
||
<Button onClick={() => setShowTemplateDialog(true)} className="gap-2">
|
||
<Plus className="h-4 w-4" />
|
||
{t("创建数据模块", "Create Data Module")}
|
||
</Button>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid gap-4 md:grid-cols-2">
|
||
<Card className="border-primary/20 bg-primary/5">
|
||
<CardHeader className="pb-3">
|
||
<div className="flex items-center gap-2">
|
||
<FileJson className="h-5 w-5 text-primary" />
|
||
<CardTitle className="text-base">{t("JSON API 模块", "JSON API Module")}</CardTitle>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<p className="text-sm text-muted-foreground mb-4">
|
||
{t(
|
||
"通过提供数据接口URL和查询参数来配置JSON API访问",
|
||
"Configure JSON API access by providing interface URL and query parameters",
|
||
)}
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="border-blue-500/20 bg-blue-500/5">
|
||
<CardHeader className="pb-3">
|
||
<div className="flex items-center gap-2">
|
||
<Cloud className="h-5 w-5 text-blue-500" />
|
||
<CardTitle className="text-base">
|
||
{t("云存储与数据库模块", "Cloud Storage & Database Module")}
|
||
</CardTitle>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<p className="text-sm text-muted-foreground mb-4">
|
||
{t(
|
||
"支持Azure、Google、AWS存储服务和各种原生数据库",
|
||
"Support Azure, Google, AWS storage services and various native databases",
|
||
)}
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
</Tabs>
|
||
</div>
|
||
|
||
{/* 生成新工具对话框 - 包含Pod配置 */}
|
||
<Dialog open={showNewToolDialog} onOpenChange={setShowNewToolDialog}>
|
||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle>{t("部署Agent", "Deploy Agent")}</DialogTitle>
|
||
<DialogDescription>
|
||
{t(
|
||
"根据Agent框架注册工具并配置Pod资源",
|
||
"Register tool based on agent framework and configure pod resources",
|
||
)}
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="space-y-4">
|
||
<div className="space-y-2">
|
||
<Label>{t("Agent名称", "Agent Name")} <span className="text-destructive">*</span></Label>
|
||
<Input
|
||
placeholder={t("输入Agent名称(小写字母、数字、连字符)", "Enter agent name (lowercase, numbers, hyphens)")}
|
||
value={toolName}
|
||
onChange={(e) => setToolName(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-'))}
|
||
/>
|
||
</div>
|
||
|
||
{/* 框架类型选择 */}
|
||
<div className="space-y-2">
|
||
<Label>{t("框架类型", "Framework Type")}</Label>
|
||
<Select value={selectedFramework} onValueChange={setSelectedFramework}>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder={t("选择框架类型(默认MCP)", "Select framework type (default MCP)")} />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{frameworkTemplates.length === 0 ? (
|
||
<SelectItem value="" disabled>
|
||
{t("加载中...", "Loading...")}
|
||
</SelectItem>
|
||
) : (
|
||
frameworkTemplates.map((template) => (
|
||
<SelectItem key={template.name} value={template.name}>
|
||
<div className="flex items-center gap-2">
|
||
<Code2 className="h-4 w-4" />
|
||
{template.displayName || template.name}
|
||
</div>
|
||
</SelectItem>
|
||
))
|
||
)}
|
||
</SelectContent>
|
||
</Select>
|
||
<p className="text-xs text-muted-foreground">
|
||
{t("选择Agent运行框架,默认为MCP", "Select agent runtime framework, defaults to MCP")}
|
||
</p>
|
||
</div>
|
||
|
||
{/* A2A 框架专用配置 */}
|
||
{selectedFramework === "A2A" && (
|
||
<div className="border rounded-lg p-4 space-y-3 bg-blue-500/5 border-blue-500/20">
|
||
<h4 className="font-medium text-sm flex items-center gap-2">
|
||
<Workflow className="h-4 w-4 text-blue-500" />
|
||
{t("A2A 框架配置", "A2A Framework Config")}
|
||
</h4>
|
||
<div className="space-y-1">
|
||
<Label className="text-sm">{t("Agent角色", "Agent Role")}</Label>
|
||
<Input
|
||
placeholder={t("如:data_analyzer", "e.g., data_analyzer")}
|
||
value={agentRole}
|
||
onChange={(e) => setAgentRole(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<Label className="text-sm">{t("Agent能力", "Agent Capabilities")}</Label>
|
||
<Input
|
||
placeholder={t("多个能力用逗号分隔,如:sql_query, data_analysis", "Comma separated, e.g., sql_query, data_analysis")}
|
||
value={agentCapabilities}
|
||
onChange={(e) => setAgentCapabilities(e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="border-t pt-4 space-y-4">
|
||
<h3 className="font-semibold flex items-center gap-2">
|
||
<Cpu className="h-4 w-4" />
|
||
{t("Pod资源配置", "Pod Resource Configuration")}
|
||
</h3>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>{t("服务网关", "Service Gateway")}</Label>
|
||
<Select value={selectedServiceGateway} onValueChange={setSelectedServiceGateway}>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder={t("选择服务网关", "Select service gateway")} />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="mcp-gateway">MCP Gateway</SelectItem>
|
||
<SelectItem value="a2a-gateway">A2A Gateway</SelectItem>
|
||
<SelectItem value="api-gateway">API Gateway</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
<p className="text-xs text-muted-foreground">
|
||
{t("选择Pod使用的服务网关", "Select service gateway for the pod")}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label>{t("选择工具", "Select Tool")} <span className="text-destructive">*</span></Label>
|
||
<Select
|
||
value={selectedDataTemplate}
|
||
onValueChange={(value) => {
|
||
setSelectedDataTemplate(value)
|
||
// 找到对应工具,可以显示其模板信息
|
||
const tool = dataTemplates.find(t => t.id === value)
|
||
if (tool?.template) {
|
||
setSelectedApiDataTemplate(tool.template)
|
||
}
|
||
}}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder={t("选择已创建的工具", "Select a created tool")} />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{dataTemplates.length === 0 ? (
|
||
<SelectItem value="" disabled>
|
||
{t("暂无可用工具,请先创建工具", "No tools available, please create a tool first")}
|
||
</SelectItem>
|
||
) : (
|
||
dataTemplates.map((tool) => (
|
||
<SelectItem key={tool.id} value={tool.id}>
|
||
<div className="flex flex-col">
|
||
<span>{tool.name}</span>
|
||
<span className="text-xs text-muted-foreground">
|
||
{tool.template || tool.category || t("工具", "Tool")}
|
||
</span>
|
||
</div>
|
||
</SelectItem>
|
||
))
|
||
)}
|
||
</SelectContent>
|
||
</Select>
|
||
<p className="text-xs text-muted-foreground">
|
||
{t("从已创建的工具中选择,系统将自动获取工具的模板和配置", "Select from created tools, system will automatically get template and config from tool")}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 动态环境变量配置(根据选择的数据存储模块) */}
|
||
{/* 注意:如果选择了已有工具,通常不需要再填写环境变量,因为系统会从工具中获取 */}
|
||
{selectedApiDataTemplate && !selectedDataTemplate && (() => {
|
||
const template = apiDataTemplates.find(t => t.template === selectedApiDataTemplate)
|
||
if (!template?.env_info) return null
|
||
// 过滤掉 OPENAI_API_KEY,系统会自动注入
|
||
const required = Object.entries(template.env_info.required || {})
|
||
.filter(([key]) => key !== "OPENAI_API_KEY")
|
||
const optional = Object.entries(template.env_info.optional || {})
|
||
.filter(([key]) => key !== "OPENAI_API_KEY")
|
||
|
||
if (required.length === 0 && optional.length === 0) return null
|
||
|
||
return (
|
||
<div className="border rounded-lg p-4 space-y-3 bg-muted/30">
|
||
<h4 className="font-medium text-sm">{t("额外环境变量配置(可选)", "Additional Environment Config (Optional)")}</h4>
|
||
<p className="text-xs text-muted-foreground">
|
||
{t("如需覆盖工具的默认配置,可在此填写", "Fill in here if you need to override tool's default config")}
|
||
</p>
|
||
{required.map(([key, desc]) => (
|
||
<div key={key} className="space-y-1">
|
||
<Label className="text-sm">{key}</Label>
|
||
<Input
|
||
placeholder={String(desc)}
|
||
value={envConfig[key] || ""}
|
||
onChange={(e) => setEnvConfig({ ...envConfig, [key]: e.target.value })}
|
||
type={key.toLowerCase().includes('password') ? 'password' : 'text'}
|
||
/>
|
||
</div>
|
||
))}
|
||
{optional.length > 0 && (
|
||
<>
|
||
<div className="border-t pt-2 mt-2">
|
||
<p className="text-xs text-muted-foreground mb-2">{t("可选配置", "Optional Config")}</p>
|
||
</div>
|
||
{optional.map(([key, desc]) => (
|
||
<div key={key} className="space-y-1">
|
||
<Label className="text-sm text-muted-foreground">{key}</Label>
|
||
<Input
|
||
placeholder={String(desc)}
|
||
value={envConfig[key] || ""}
|
||
onChange={(e) => setEnvConfig({ ...envConfig, [key]: e.target.value })}
|
||
/>
|
||
</div>
|
||
))}
|
||
</>
|
||
)}
|
||
</div>
|
||
)
|
||
})()}
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>{t("Agent个数", "Agent Count")}</Label>
|
||
<Input
|
||
type="number"
|
||
min="1"
|
||
max="10"
|
||
value={podConfig.agentCount}
|
||
onChange={(e) => setPodConfig({ ...podConfig, agentCount: Number.parseInt(e.target.value) })}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label>{t("CPU核数", "CPU Cores")}</Label>
|
||
<Input
|
||
type="number"
|
||
min="1"
|
||
max="16"
|
||
value={podConfig.cpuCores}
|
||
onChange={(e) => setPodConfig({ ...podConfig, cpuCores: Number.parseInt(e.target.value) })}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label>{t("内存大小 (GB)", "Memory Size (GB)")}</Label>
|
||
<Input
|
||
type="number"
|
||
min="1"
|
||
max="64"
|
||
value={podConfig.memoryGB}
|
||
onChange={(e) => setPodConfig({ ...podConfig, memoryGB: Number.parseInt(e.target.value) })}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label>{t("可扩展Agent数量", "Scalable Agent Count")}</Label>
|
||
<Input
|
||
type="number"
|
||
min="1"
|
||
max="100"
|
||
value={podConfig.maxAgents}
|
||
onChange={(e) => setPodConfig({ ...podConfig, maxAgents: Number.parseInt(e.target.value) })}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label>{t("使用的模型", "Model to Use")}</Label>
|
||
<Select value={podConfig.model} onValueChange={(value) => setPodConfig({ ...podConfig, model: value })}>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder={t("选择模型", "Select model")} />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{availableModels.length === 0 ? (
|
||
<SelectItem value="" disabled>
|
||
{t("加载中...", "Loading...")}
|
||
</SelectItem>
|
||
) : (
|
||
availableModels.map((model) => (
|
||
<SelectItem key={model.id} value={model.id}>
|
||
<div className="flex flex-col">
|
||
<span>{model.name}</span>
|
||
{model.description && (
|
||
<span className="text-xs text-muted-foreground">{model.description}</span>
|
||
)}
|
||
</div>
|
||
</SelectItem>
|
||
))
|
||
)}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="rounded-lg bg-muted p-4 text-sm">
|
||
<p className="font-medium mb-2">{t("配置摘要", "Configuration Summary")}</p>
|
||
<div className="grid grid-cols-2 gap-2 text-muted-foreground">
|
||
<div className="col-span-2">
|
||
{t("Agent名称", "Agent Name")}: {toolName || t("未填写", "Not filled")}
|
||
</div>
|
||
<div className="col-span-2">
|
||
{t("使用工具", "Tool")}: {
|
||
selectedDataTemplate
|
||
? (dataTemplates.find(t => t.id === selectedDataTemplate)?.name || selectedDataTemplate)
|
||
: t("未选择", "Not selected")
|
||
}
|
||
</div>
|
||
<div className="col-span-2">
|
||
{t("工具模板", "Tool Template")}: {
|
||
selectedDataTemplate
|
||
? (dataTemplates.find(t => t.id === selectedDataTemplate)?.template || "-")
|
||
: "-"
|
||
}
|
||
</div>
|
||
<div className="col-span-2">
|
||
{t("框架", "Framework")}: {selectedFramework || "MCP"}
|
||
</div>
|
||
<div>
|
||
{t("CPU", "CPU")}: {podConfig.cpuCores} {t("核", "cores")}
|
||
</div>
|
||
<div>
|
||
{t("内存", "Memory")}: {podConfig.memoryGB}GB
|
||
</div>
|
||
<div>
|
||
{t("模型", "Model")}: {podConfig.model}
|
||
</div>
|
||
{selectedFramework === "A2A" && agentRole && (
|
||
<div className="col-span-2">
|
||
{t("Agent角色", "Agent Role")}: {agentRole}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setShowNewToolDialog(false)} disabled={deploying}>
|
||
{t("取消", "Cancel")}
|
||
</Button>
|
||
<Button onClick={handleDeployAgent} disabled={deploying || !selectedDataTemplate || !toolName}>
|
||
{deploying ? t("部署中...", "Deploying...") : t("部署Agent", "Deploy Agent")}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* 数据模块对话框 */}
|
||
<Dialog open={showTemplateDialog} onOpenChange={setShowTemplateDialog}>
|
||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle>{t("创建数据模块", "Create Data Module")}</DialogTitle>
|
||
<DialogDescription>
|
||
{t("选择模块类型并填写配置信息", "Select module type and fill in configuration information")}
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="space-y-4">
|
||
<div className="space-y-2">
|
||
<Label>{t("模块类型", "Module Type")}</Label>
|
||
<Select value={templateType} onValueChange={(value: "json_api" | "cloud_storage" | "database" | "data_storage") => {
|
||
setTemplateType(value as any)
|
||
// 重置配置
|
||
setTemplateConfig({
|
||
url: "",
|
||
apiKey: "",
|
||
queryParams: "",
|
||
jsonContent: "",
|
||
service: "",
|
||
dbType: "",
|
||
connectionString: "",
|
||
})
|
||
setSelectedApiDataTemplate("")
|
||
setEnvConfig({})
|
||
}}>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{/* 数据库模块(来自API) */}
|
||
{apiDataTemplates.length > 0 && (
|
||
<SelectItem value="data_storage">
|
||
<div className="flex items-center gap-2">
|
||
<Boxes className="h-4 w-4" />
|
||
{t("数据库模块", "Database Module")}
|
||
</div>
|
||
</SelectItem>
|
||
)}
|
||
<SelectItem value="json_api">
|
||
<div className="flex items-center gap-2">
|
||
<FileJson className="h-4 w-4" />
|
||
{t("JSON API 模块", "JSON API Module")}
|
||
</div>
|
||
</SelectItem>
|
||
<SelectItem value="cloud_storage">
|
||
<div className="flex items-center gap-2">
|
||
<Cloud className="h-4 w-4" />
|
||
{t("云存储模块", "Cloud Storage Module")}
|
||
</div>
|
||
</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
{/* 数据库模块(来自API的dataTemplates) */}
|
||
{templateType === "data_storage" as any ? (
|
||
<>
|
||
<div className="space-y-2">
|
||
<Label>{t("选择数据库模块", "Select Database Module")} <span className="text-destructive">*</span></Label>
|
||
<Select
|
||
value={selectedApiDataTemplate}
|
||
onValueChange={(value) => {
|
||
setSelectedApiDataTemplate(value)
|
||
setEnvConfig({})
|
||
// 自动填充模块名称
|
||
const template = apiDataTemplates.find(t => t.template === value)
|
||
if (template) {
|
||
setTemplateName(value)
|
||
}
|
||
}}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder={t("选择模块", "Select module")} />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{apiDataTemplates.map((template) => (
|
||
<SelectItem key={template.template} value={template.template}>
|
||
<div className="flex flex-col">
|
||
<span className="font-medium">{template.template}</span>
|
||
{template.description && (
|
||
<span className="text-xs text-muted-foreground">{template.description}</span>
|
||
)}
|
||
</div>
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
{/* 模块名称(自定义) */}
|
||
<div className="space-y-2">
|
||
<Label>{t("模块名称", "Module Name")} <span className="text-destructive">*</span></Label>
|
||
<Input
|
||
placeholder={t("输入模块名称", "Enter module name")}
|
||
value={templateName}
|
||
onChange={(e) => setTemplateName(e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
{/* 动态环境变量配置 - 根据后端返回的 env_info 生成,过滤掉 OPENAI_API_KEY(系统自动注入) */}
|
||
{selectedApiDataTemplate && (() => {
|
||
const template = apiDataTemplates.find(t => t.template === selectedApiDataTemplate)
|
||
if (!template?.env_info) return null
|
||
// 过滤掉 OPENAI_API_KEY,系统会自动注入用户的 LiteLLM 密钥
|
||
const required = Object.entries(template.env_info.required || {})
|
||
.filter(([key]) => key !== "OPENAI_API_KEY")
|
||
const optional = Object.entries(template.env_info.optional || {})
|
||
.filter(([key]) => key !== "OPENAI_API_KEY")
|
||
return (
|
||
<div className="border rounded-lg p-4 space-y-3 bg-muted/30">
|
||
<h4 className="font-medium text-sm">{t("连接配置", "Connection Config")}</h4>
|
||
<p className="text-xs text-muted-foreground">
|
||
{t("注意:API密钥由系统自动注入,无需填写", "Note: API key is automatically injected by system, no need to fill in")}
|
||
</p>
|
||
{/* 只渲染后端返回的必填字段(排除 OPENAI_API_KEY) */}
|
||
{required.map(([key, desc]) => (
|
||
<div key={key} className="space-y-1">
|
||
<Label className="text-sm">{key} <span className="text-destructive">*</span></Label>
|
||
<Input
|
||
placeholder={String(desc)}
|
||
value={envConfig[key] || ""}
|
||
onChange={(e) => setEnvConfig({ ...envConfig, [key]: e.target.value })}
|
||
type={key.toLowerCase().includes('password') ? 'password' : 'text'}
|
||
/>
|
||
</div>
|
||
))}
|
||
{/* 只渲染后端返回的可选字段(排除 OPENAI_API_KEY) */}
|
||
{optional.length > 0 && (
|
||
<>
|
||
<div className="border-t pt-2 mt-2">
|
||
<p className="text-xs text-muted-foreground mb-2">{t("可选配置", "Optional Config")}</p>
|
||
</div>
|
||
{optional.map(([key, desc]) => (
|
||
<div key={key} className="space-y-1">
|
||
<Label className="text-sm text-muted-foreground">{key}</Label>
|
||
<Input
|
||
placeholder={String(desc)}
|
||
value={envConfig[key] || ""}
|
||
onChange={(e) => setEnvConfig({ ...envConfig, [key]: e.target.value })}
|
||
/>
|
||
</div>
|
||
))}
|
||
</>
|
||
)}
|
||
</div>
|
||
)
|
||
})()}
|
||
</>
|
||
) : (
|
||
<>
|
||
<div className="space-y-2">
|
||
<Label>{t("模块名称", "Module Name")}</Label>
|
||
<Input
|
||
placeholder={t("输入模块名称", "Enter module name")}
|
||
value={templateName}
|
||
onChange={(e) => setTemplateName(e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
{templateType === "json_api" ? (
|
||
<>
|
||
<div className="flex items-center space-x-2">
|
||
<Checkbox
|
||
id="useJsonUpload"
|
||
checked={useJsonUpload}
|
||
onCheckedChange={(checked) => setUseJsonUpload(checked === true)}
|
||
/>
|
||
<Label htmlFor="useJsonUpload" className="text-sm font-normal cursor-pointer">
|
||
{t("上传JSON文件", "Upload JSON File")}
|
||
</Label>
|
||
</div>
|
||
|
||
{useJsonUpload ? (
|
||
<>
|
||
<div className="space-y-2">
|
||
<Label>{t("选择JSON文件", "Select JSON File")}</Label>
|
||
<div className="flex items-center gap-2">
|
||
<Input
|
||
type="file"
|
||
accept=".json,application/json"
|
||
className="flex-1"
|
||
/>
|
||
<Button variant="outline" size="sm" className="gap-1">
|
||
<Upload className="h-4 w-4" />
|
||
{t("上传", "Upload")}
|
||
</Button>
|
||
</div>
|
||
<p className="text-xs text-muted-foreground">
|
||
{t("通过APILLAMA自动识别请求参数", "Automatically identify request parameters via APILLAMA")}
|
||
</p>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<>
|
||
<div className="space-y-2">
|
||
<Label>{t("数据接口URL", "Data Interface URL")}</Label>
|
||
<Input
|
||
placeholder="https://api.example.com/data"
|
||
value={templateConfig.url}
|
||
onChange={(e) => setTemplateConfig({ ...templateConfig, url: e.target.value })}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>{t("密钥Key", "API Key")}</Label>
|
||
<Input
|
||
placeholder={t("输入API密钥", "Enter API Key")}
|
||
type="password"
|
||
value={templateConfig.apiKey}
|
||
onChange={(e) => setTemplateConfig({ ...templateConfig, apiKey: e.target.value })}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>{t("查询参数", "Query Parameters")}</Label>
|
||
<textarea
|
||
className="w-full h-24 rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||
placeholder={t(
|
||
"输入查询参数,例如: page=1&limit=10",
|
||
"Enter query parameters, e.g., page=1&limit=10",
|
||
)}
|
||
value={templateConfig.queryParams}
|
||
onChange={(e) => setTemplateConfig({ ...templateConfig, queryParams: e.target.value })}
|
||
/>
|
||
</div>
|
||
</>
|
||
)}
|
||
</>
|
||
) : templateType === "cloud_storage" && (
|
||
<>
|
||
<div className="space-y-2">
|
||
<Label>{t("存储服务", "Storage Service")}</Label>
|
||
<Select value={templateConfig.service} onValueChange={(value) => setTemplateConfig({ ...templateConfig, service: value })}>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder={t("选择服务", "Select service")} />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="azure_blob">Azure Blob Storage</SelectItem>
|
||
<SelectItem value="aws_s3">AWS S3</SelectItem>
|
||
<SelectItem value="aliyun_oss">阿里云OSS</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>{t("连接字符串", "Connection String")}</Label>
|
||
<Input
|
||
placeholder={t("输入连接字符串", "Enter connection string")}
|
||
type="password"
|
||
value={templateConfig.connectionString}
|
||
onChange={(e) => setTemplateConfig({ ...templateConfig, connectionString: e.target.value })}
|
||
/>
|
||
</div>
|
||
</>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setShowTemplateDialog(false)} disabled={creatingTemplate}>
|
||
{t("取消", "Cancel")}
|
||
</Button>
|
||
<Button
|
||
onClick={handleCreateTemplate}
|
||
disabled={creatingTemplate || !templateName || (templateType === "data_storage" as any && !selectedApiDataTemplate)}
|
||
>
|
||
{creatingTemplate ? t("创建中...", "Creating...") : t("创建模块", "Create Module")}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
</DashboardLayout>
|
||
</AuthGuard>
|
||
)
|
||
}
|