package flow import ( "ai-agent/workflow/model/entity" "net/url" "path" "path/filepath" "regexp" "strconv" "strings" ) // FindEndNodes 从指定起始节点开始,遍历图找到所有末端节点(没有出边的节点) func FindEndNodes(startNodeId string, edges []entity.FlowEdge) []string { nextMap := make(map[string][]string) for _, e := range edges { nextMap[e.From] = append(nextMap[e.From], e.To) } endNodeSet := make(map[string]struct{}) visited := make(map[string]struct{}) queue := []string{startNodeId} for len(queue) > 0 { node := queue[0] queue = queue[1:] if _, exist := visited[node]; exist { continue } visited[node] = struct{}{} nextList := nextMap[node] if len(nextList) == 0 { endNodeSet[node] = struct{}{} continue } queue = append(queue, nextList...) } res := make([]string, 0, len(endNodeSet)) for k := range endNodeSet { res = append(res, k) } return res } // ExtractFlowNodeFrom 从 FlowInfo 中提取节点列表,并自动补齐 DataMerge 节点的 InputSource func ExtractFlowNodeFrom(flowContent *entity.FlowInfo) []*entity.FlowNode { // 构建每个节点的上游节点映射 upstreamMap := make(map[string][]string) for _, edge := range flowContent.Edges { upstreamMap[edge.To] = append(upstreamMap[edge.To], edge.From) } // 同时更新 flowContent.Nodes 中的 DataMerge 节点 //for i := range flowContent.Nodes { // n := &flowContent.Nodes[i] // // 对于 DataMerge 节点,自动根据边关系填充 InputSource // if n.NodeCode == node.NodeTypeDataMerge { // n.InputSource = nil // for _, fromId := range upstreamMap[n.Id] { // n.InputSource = append(n.InputSource, entity.FlowNodeInputSource{ // NodeId: fromId, // }) // } // } //} var flowNodes []*entity.FlowNode for _, item := range flowContent.Nodes { flowNodes = append(flowNodes, &item) } return flowNodes } // GetFileTypeByPath 根据文件路径/URL的后缀名判断文件类型 func GetFileTypeByPath(filePath string) string { if filePath == "" { return "" } // 解析 URL,获取真实路径(兼容 http 链接) u, err := url.Parse(filePath) if err == nil { filePath = u.Path } // 获取后缀(小写) ext := filepath.Ext(filePath) ext = strings.ToLower(ext) // 判断类型 switch ext { case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp": return "image" case ".mp4", ".mov", ".avi", ".flv", ".wmv", ".mkv": return "video" case ".mp3", ".wav", ".m4a", ".flac", ".aac", ".ogg": return "audio" case ".txt", ".md", ".log", ".json", ".xml", ".inc": return "text" case ".html": return "html" case ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx": return "document" default: return "" } } // GetUrlSuffix 获取URL文件后缀 // rawUrl: 原始链接 // withDot: true 返回 .mp4 false 返回 mp4 func GetUrlSuffix(rawUrl string, withDot bool) string { // 解析URL,剥离查询参数 u, err := url.Parse(rawUrl) if err != nil { return "" } // 提取路径部分 filePath := u.Path // 获取文件名 fileName := path.Base(filePath) if fileName == "" || !strings.Contains(fileName, ".") { return "" } // 截取后缀 suffix := path.Ext(fileName) if !withDot { suffix = strings.TrimPrefix(suffix, ".") } return suffix } // ExtractImageCount 修复:支持单引号/双引号 + 换行 + 空格 func ExtractImageCount(content string) int { // 🔥 关键:支持 class='image-count' (单引号) re := regexp.MustCompile(`
]*>.*?(\d+).*?
`) match := re.FindStringSubmatch(content) if len(match) >= 2 { num, err := strconv.Atoi(match[1]) if err == nil { return num } } return 0 } func ImageTagRegex(html string) string { // 🔥 修复:支持单引号、双引号、空格、换行,100% 删除imageTagRegex := regexp.MustCompile(`
]*>[\s\S]*?
`) return imageTagRegex.ReplaceAllString(html, "") } // StripHtmlTags 去掉所有HTML标签,保留换行和文本结构,并删除配图标记行 func StripHtmlTags(html string) string { // 1. 替换块级标签为换行,保证排版 blockTags := regexp.MustCompile(`?(div|p|h1|h2|h3|h4|h5|h6|li|ul|ol|br|tr|td|th)[^>]*>`) text := blockTags.ReplaceAllString(html, "\n") // 2. 去掉所有剩余的 HTML 标签 allTags := regexp.MustCompile(`<[^>]+>`) text = allTags.ReplaceAllString(text, "") // 4. 清理多余空行(多个换行只保留一个) text = regexp.MustCompile(`\n\s*\n`).ReplaceAllString(text, "\n") // 5. 只去掉首尾空白,中间换行保留 text = strings.TrimSpace(text) return text } // SplitMultiContents 拆分模型返回的多条文案(基于HTML标签分隔) func SplitMultiContents(htmlContent string) []string { var contents []string // 正则匹配