218 lines
6.0 KiB
Go
218 lines
6.0 KiB
Go
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(`<p class=['"]image-count['"][^>]*>.*?(\d+).*?</p>`)
|
|
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% 删除 <p class='image-count'>
|
|
imageTagRegex := regexp.MustCompile(`<p class=['"]image-count['"][^>]*>[\s\S]*?</p>`)
|
|
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
|
|
// 正则匹配<div class="content-item" id="content-{序号}">包裹的内容
|
|
re := regexp.MustCompile(`<div class="content-item" id="content-\d+">([\s\S]*?)</div>`)
|
|
matches := re.FindAllStringSubmatch(htmlContent, -1)
|
|
for _, match := range matches {
|
|
if len(match) > 1 {
|
|
// 清理空内容
|
|
trimmed := strings.TrimSpace(match[1])
|
|
if trimmed != "" {
|
|
contents = append(contents, trimmed)
|
|
}
|
|
}
|
|
}
|
|
// 兜底:如果没有匹配到结构化内容,按换行/分隔符拆分
|
|
if len(contents) == 0 {
|
|
contents = strings.Split(htmlContent, "===分隔符===") // 提示词中可新增此兜底规则
|
|
}
|
|
return contents
|
|
}
|
|
|
|
// GetAllImgSrcFromHtml 先把提取img src的工具方法放在外面
|
|
func GetAllImgSrcFromHtml(html string) []string {
|
|
var imgSrcList []string
|
|
re := regexp.MustCompile(`<img[^>]*src\s*=\s*["']([^"']+)["']`)
|
|
submatch := re.FindAllStringSubmatch(html, -1)
|
|
for _, match := range submatch {
|
|
if len(match) >= 2 {
|
|
imgSrcList = append(imgSrcList, match[1])
|
|
}
|
|
}
|
|
return imgSrcList
|
|
}
|
|
|
|
// ReplaceImgSrc 替换img src的方法
|
|
func ReplaceImgSrc(html string, oldSrc string, newSrc string) string {
|
|
// 精准替换:找到 <img xxx src="oldSrc" xxx>
|
|
re := regexp.MustCompile(`(<img[^>]*src\s*=\s*["'])` + regexp.QuoteMeta(oldSrc) + `(["'])`)
|
|
return re.ReplaceAllString(html, `${1}`+newSrc+`${2}`)
|
|
}
|