diff --git a/internal/bootstrap/routes.go b/internal/bootstrap/routes.go index efc7f9f..d610bf4 100644 --- a/internal/bootstrap/routes.go +++ b/internal/bootstrap/routes.go @@ -229,10 +229,10 @@ func registerDashboardAIAgentRoutes(group *gin.RouterGroup) { } func registerDashboardAIWorkflowRoutes(group *gin.RouterGroup) { - group.GET("/:id", dashboard.AIWorkflowGetBy) group.POST("/create", dashboard.AIWorkflowPostCreate) group.POST("/update", dashboard.AIWorkflowPostUpdate) group.POST("/delete", dashboard.AIWorkflowPostDelete) + group.POST("/restore-version", dashboard.AIWorkflowPostRestoreVersion) group.Any("/list", dashboard.AIWorkflowAnyList) group.GET("/node-spec/list", dashboard.AIWorkflowGetNodeSpecList) group.GET("/default-definition", dashboard.AIWorkflowGetDefaultDefinition) @@ -242,6 +242,8 @@ func registerDashboardAIWorkflowRoutes(group *gin.RouterGroup) { group.GET("/run/:id", dashboard.AIWorkflowGetRunBy) group.Any("/version/list", dashboard.AIWorkflowAnyVersionList) group.GET("/version/:id", dashboard.AIWorkflowGetVersionBy) + group.GET("/:id/usage", dashboard.AIWorkflowGetUsage) + group.GET("/:id", dashboard.AIWorkflowGetBy) } func registerDashboardAgentRunRoutes(group *gin.RouterGroup) { diff --git a/internal/handlers/dashboard/ai_workflow_handler.go b/internal/handlers/dashboard/ai_workflow_handler.go index 980f4f4..a5cdacf 100644 --- a/internal/handlers/dashboard/ai_workflow_handler.go +++ b/internal/handlers/dashboard/ai_workflow_handler.go @@ -99,6 +99,44 @@ func AIWorkflowPostDelete(ctx *gin.Context) { httpx.WriteJSON(ctx, nil) } +func AIWorkflowPostRestoreVersion(ctx *gin.Context) { + operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentUpdate) + if err != nil { + httpx.WriteJSON(ctx, err) + return + } + req := request.RestoreAIWorkflowVersionRequest{} + if err := params.ReadJSON(ctx, &req); err != nil { + httpx.WriteJSON(ctx, err) + return + } + if err := services.AIWorkflowService.RestoreVersion(req, operator); err != nil { + httpx.WriteJSON(ctx, err) + return + } + httpx.WriteJSON(ctx, nil) +} + +func AIWorkflowGetUsage(ctx *gin.Context) { + id, ok := httpx.GetPathInt64(ctx, "id") + if !ok { + return + } + if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil { + httpx.WriteJSON(ctx, err) + return + } + items := services.AIWorkflowService.ListUsage(id) + ret := make([]response.AIWorkflowUsageResponse, 0, len(items)) + for _, item := range items { + if item.Agent == nil || item.Version == nil { + continue + } + ret = append(ret, response.AIWorkflowUsageResponse{AIAgentID: item.Agent.ID, AIAgentName: item.Agent.Name, WorkflowVersionID: item.Binding.WorkflowVersionID, WorkflowVersion: item.Version.Version, Enabled: item.Binding.Enabled}) + } + httpx.WriteJSON(ctx, ret) +} + func AIWorkflowGetNodeSpecList(ctx *gin.Context) { if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil { httpx.WriteJSON(ctx, err) diff --git a/internal/pkg/dto/request/ai_workflow_request.go b/internal/pkg/dto/request/ai_workflow_request.go index 9794fc3..ec9b82d 100644 --- a/internal/pkg/dto/request/ai_workflow_request.go +++ b/internal/pkg/dto/request/ai_workflow_request.go @@ -29,3 +29,8 @@ type PublishAIWorkflowRequest struct { type AIWorkflowVersionListRequest struct { WorkflowID int64 `json:"workflowId"` } + +type RestoreAIWorkflowVersionRequest struct { + WorkflowID int64 `json:"workflowId"` + WorkflowVersionID int64 `json:"workflowVersionId"` +} diff --git a/internal/pkg/dto/response/ai_workflow_response.go b/internal/pkg/dto/response/ai_workflow_response.go index f050452..519c662 100644 --- a/internal/pkg/dto/response/ai_workflow_response.go +++ b/internal/pkg/dto/response/ai_workflow_response.go @@ -47,6 +47,14 @@ type AIWorkflowTemplateResponse struct { Definition dsl.Definition `json:"definition"` } +type AIWorkflowUsageResponse struct { + AIAgentID int64 `json:"aiAgentId"` + AIAgentName string `json:"aiAgentName"` + WorkflowVersionID int64 `json:"workflowVersionId"` + WorkflowVersion int `json:"workflowVersion"` + Enabled bool `json:"enabled"` +} + type AIWorkflowNodeSpecResponse struct { Type string `json:"type"` Title string `json:"title"` diff --git a/internal/repositories/ai_agent_workflow_binding_repository.go b/internal/repositories/ai_agent_workflow_binding_repository.go index bd13b0e..d21679d 100644 --- a/internal/repositories/ai_agent_workflow_binding_repository.go +++ b/internal/repositories/ai_agent_workflow_binding_repository.go @@ -30,6 +30,14 @@ func (r *aiAgentWorkflowBindingRepository) FindEnabledByAgentID(db *gorm.DB, age return ret } +func (r *aiAgentWorkflowBindingRepository) FindByWorkflowID(db *gorm.DB, workflowID int64) []models.AIAgentWorkflowBinding { + ret := make([]models.AIAgentWorkflowBinding, 0) + if workflowID > 0 { + db.Where("workflow_id = ?", workflowID).Order("priority ASC, id ASC").Find(&ret) + } + return ret +} + func (r *aiAgentWorkflowBindingRepository) CountByWorkflowID(db *gorm.DB, workflowID int64) int64 { var count int64 if workflowID > 0 { diff --git a/internal/services/ai_workflow_service.go b/internal/services/ai_workflow_service.go index 89d7d92..9494505 100644 --- a/internal/services/ai_workflow_service.go +++ b/internal/services/ai_workflow_service.go @@ -48,6 +48,12 @@ type AIWorkflowTemplate struct { Definition dsl.Definition } +type AIWorkflowUsageItem struct { + Binding models.AIAgentWorkflowBinding + Agent *models.AIAgent + Version *models.AIWorkflowVersion +} + func (s *aiWorkflowService) Get(id int64) *models.AIWorkflow { if id <= 0 { return nil @@ -263,6 +269,27 @@ func (s *aiWorkflowService) DeleteWorkflow(id int64, operator *dto.AuthPrincipal }) } +func (s *aiWorkflowService) RestoreVersion(req request.RestoreAIWorkflowVersionRequest, operator *dto.AuthPrincipal) error { + if operator == nil { + return errorsx.UnauthorizedI18n("error.auth.expired") + } + workflow := s.Get(req.WorkflowID) + version := s.GetVersion(req.WorkflowVersionID) + if workflow == nil || version == nil || version.WorkflowID != workflow.ID { + return errorsx.InvalidParamI18n("error.e0002") + } + return repositories.AIWorkflowRepository.Updates(sqls.DB(), workflow.ID, map[string]any{"draft_definition": version.Definition, "update_user_id": operator.UserID, "update_user_name": operator.Username, "updated_at": time.Now()}) +} + +func (s *aiWorkflowService) ListUsage(workflowID int64) []AIWorkflowUsageItem { + bindings := repositories.AIAgentWorkflowBindingRepository.FindByWorkflowID(sqls.DB(), workflowID) + ret := make([]AIWorkflowUsageItem, 0, len(bindings)) + for _, binding := range bindings { + ret = append(ret, AIWorkflowUsageItem{Binding: binding, Agent: repositories.AIAgentRepository.Get(sqls.DB(), binding.AIAgentID), Version: repositories.AIWorkflowVersionRepository.Get(sqls.DB(), binding.WorkflowVersionID)}) + } + return ret +} + func (s *aiWorkflowService) PublishWorkflow(req request.PublishAIWorkflowRequest, operator *dto.AuthPrincipal) (*models.AIWorkflowVersion, error) { if operator == nil { return nil, errorsx.UnauthorizedI18n("error.auth.expired") diff --git a/web/app/dashboard/ai-agents/_components/config-workbench.tsx b/web/app/dashboard/ai-agents/_components/config-workbench.tsx index 831b97e..9aabcf1 100644 --- a/web/app/dashboard/ai-agents/_components/config-workbench.tsx +++ b/web/app/dashboard/ai-agents/_components/config-workbench.tsx @@ -38,7 +38,6 @@ import { createAIAgent, fetchAIAgent, fetchAIAgentRevisions, - fetchAIAgentWorkflow, fetchAIConfigsAll, fetchKnowledgeBasesAll, fetchAIWorkflowDefaultDefinition, @@ -49,11 +48,9 @@ import { fetchAgentTeamsAll, fetchMCPCatalog, fetchSkillDefinitionsAll, - publishAIAgentWorkflow, publishAIAgent, rollbackAIAgent, rollbackAIAgentRollout, - saveAIAgentWorkflow, updateAIAgent, validateAIWorkflow, type AIAgent, @@ -473,23 +470,6 @@ export function AIAgentConfigWorkbench({ } } - async function saveWorkflowDraft() { - if (!currentAgentId) return - setSavingWorkflow(true) - try { - await saveAIAgentWorkflow({ - name: "", - description: "", - definition, - }) - toast.success("Workflow draft saved") - } catch (error) { - toast.error(error instanceof Error ? error.message : "Failed to save workflow draft") - } finally { - setSavingWorkflow(false) - } - } - async function rollbackAgentRevision(revisionId: number) { if (!agent || revisionId <= 0 || revisionId === agent.publishedRevisionId) return setSavingAgent(true) @@ -520,74 +500,6 @@ export function AIAgentConfigWorkbench({ } } - async function validateWorkflowDraft() { - setSavingWorkflow(true) - try { - const result = await validateAIWorkflow(definition) - toast[result.valid ? "success" : "error"]( - result.valid ? "Workflow is valid" : "Workflow has validation errors" - ) - } catch (error) { - toast.error(error instanceof Error ? error.message : "Failed to validate workflow") - } finally { - setSavingWorkflow(false) - } - } - - async function restoreDefaultWorkflow() { - if (savingWorkflow || loading) return - setSavingWorkflow(true) - try { - const defaultDefinition = await fetchAIWorkflowDefaultDefinition() - replaceWorkflowDefinition(defaultDefinition ?? fallbackDefinition) - toast.success("已恢复默认流程,保存草稿或发布后生效") - } catch (error) { - toast.error(error instanceof Error ? error.message : "恢复默认流程失败") - } finally { - setSavingWorkflow(false) - } - } - - function applySelectedWorkflowTemplate() { - const template = workflowTemplates.find((item) => item.code === selectedWorkflowTemplate) - if (!template) return - replaceWorkflowDefinition(template.definition) - toast.success(`已应用 ${template.name} 模板,保存草稿或发布后生效`) - } - - async function publishWorkflow() { - if (!currentAgentId) return - setSavingWorkflow(true) - try { - const saved = await saveAIAgentWorkflow({ - name: "", - description: "", - definition, - }) - const version = await publishAIAgentWorkflow(currentAgentId, definition) - toast.success(`Published version ${version.version}`) - setAgent((current) => - current ? { ...current, workflowVersionId: version.id } : current - ) - if (saved.id > 0) { - const versionPage = await fetchAIWorkflowVersions({ - workflowId: saved.id, - limit: 20, - }) - setWorkflowVersions(versionPage.results ?? []) - } else { - setWorkflowVersions((current) => - current.some((item) => item.id === version.id) ? current : [version, ...current] - ) - } - onAgentSaved?.() - } catch (error) { - toast.error(error instanceof Error ? error.message : "Failed to publish workflow") - } finally { - setSavingWorkflow(false) - } - } - const sections: { key: SectionKey; title: string; icon: ReactNode }[] = [ { key: "basic", title: "基础信息", icon: }, { key: "capabilities", title: "能力来源", icon: }, diff --git a/web/app/dashboard/ai-workflows/page.tsx b/web/app/dashboard/ai-workflows/page.tsx index 57281c2..db486a8 100644 --- a/web/app/dashboard/ai-workflows/page.tsx +++ b/web/app/dashboard/ai-workflows/page.tsx @@ -1,99 +1,36 @@ "use client" -import { useCallback, useEffect, useState } from "react" -import { PlusIcon } from "lucide-react" +import { useCallback, useEffect, useMemo, useState } from "react" +import { ArrowLeftIcon, PlusIcon, SearchIcon } from "lucide-react" import { toast } from "sonner" +import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Textarea } from "@/components/ui/textarea" -import { - createAIWorkflow, - fetchAIWorkflow, - fetchAIWorkflowNodeSpecs, - fetchAIWorkflows, - publishAIWorkflow, - updateAIWorkflow, - validateAIWorkflow, - type AIWorkflow, - type AIWorkflowDefinition, - type AIWorkflowNodeSpec, -} from "@/lib/api/admin" +import { cn, formatDateTime } from "@/lib/utils" +import { createAIWorkflow, deleteAIWorkflow, fetchAIWorkflow, fetchAIWorkflowNodeSpecs, fetchAIWorkflows, fetchAIWorkflowUsage, fetchAIWorkflowVersions, publishAIWorkflow, restoreAIWorkflowVersion, updateAIWorkflow, validateAIWorkflow, type AIWorkflow, type AIWorkflowDefinition, type AIWorkflowNodeSpec, type AIWorkflowUsage, type AIWorkflowVersion } from "@/lib/api/admin" import { WorkflowEditor } from "./_components/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" }], -} +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 default function DashboardAIWorkflowsPage() { - const [items, setItems] = useState([]) - const [nodeSpecs, setNodeSpecs] = useState([]) - const [active, setActive] = useState(null) - const [name, setName] = useState("") - const [description, setDescription] = useState("") - const [definition, setDefinition] = useState(emptyDefinition) - const [saving, setSaving] = useState(false) - - const select = useCallback(async (id: number) => { - const item = await fetchAIWorkflow(id) - setActive(item) - setName(item.name) - setDescription(item.description) - setDefinition(item.draftDefinition) - }, []) - - const reload = useCallback(async () => { - const [page, specs] = await Promise.all([fetchAIWorkflows({ limit: 100 }), fetchAIWorkflowNodeSpecs()]) - setItems(page.results) - setNodeSpecs(specs) - if (!active && page.results[0]) await select(page.results[0].id) - }, [active, select]) - - useEffect(() => { void reload().catch((error) => toast.error(error instanceof Error ? error.message : "加载工作流失败")) }, [reload]) - - async function save() { - if (!name.trim()) { toast.error("请填写工作流名称"); return } - setSaving(true) - try { - if (active) { - await updateAIWorkflow({ id: active.id, name: name.trim(), description: description.trim(), definition }) - await select(active.id) - } else { - const created = await createAIWorkflow({ name: name.trim(), description: description.trim(), definition }) - await select(created.id) - } - await reload() - toast.success("工作流草稿已保存") - } catch (error) { toast.error(error instanceof Error ? error.message : "保存工作流失败") } finally { setSaving(false) } - } - - async function publish() { - if (!active) { toast.error("请先保存工作流草稿"); return } - setSaving(true) - try { - const version = await publishAIWorkflow(active.id, definition) - await select(active.id) - await reload() - toast.success(`已发布工作流 v${version.version}`) - } catch (error) { toast.error(error instanceof Error ? error.message : "发布工作流失败") } finally { setSaving(false) } - } - - function create() { setActive(null); setName(""); setDescription(""); setDefinition(emptyDefinition) } - - return
- -
-
setName(event.target.value)} placeholder="工作流名称" className="max-w-sm" />