package flow import ( "ai-agent/gateway" "context" "encoding/base64" "fmt" "strings" "gitea.redpowerfuture.com/red-future/common/oss" "github.com/gogf/gf/v2/util/gconv" "github.com/google/uuid" ) // resolveSaveFileResult 解析结果值为可入库的 URL: // - 已是 http(s) URL 或 MinIO 对象裸路径 → 直接返回 // - 非路径(base64 图片/文本)→ 上传 OSS 换取 URL func resolveSaveFileResult(ctx context.Context, val any) (string, error) { isPath, path, fileBytes, ext := resolveFileContent(val) if isPath { return path, nil } if ext == "" { ext = ".png" } fileUrl, err := gateway.Upload(ctx, fmt.Sprintf("workflow_result_%s%s", uuid.NewString(), ext), fileBytes) if err != nil { return "", err } return fileUrl, nil } // resolveFileContent 判断结果值形态: // - 已是 URL 路径(http/https 开头)→ 直接使用 // - data URI(data:;base64,)→ 解码为字节,扩展名按 mime 推断 // - 纯 base64(可解码且长度足以认为是编码数据)→ 解码为字节,默认 .png // - 其余(文本)→ 以 .inc 扩展名上传原文 func resolveFileContent(val any) (isPath bool, path string, fileBytes []byte, ext string) { s := gconv.String(val) if isFileURL(s) { return true, s, nil, "" } // MinIO 对象裸路径(无 http 前缀,模型网关转存 OSS 后返回) if oss.IsOSSPath(s) { return true, s, nil, "" } // data URI:data:;base64, if b, mime, ok := parseDataURI(s); ok { return false, "", b, extOfMime(mime) } // 纯 base64:可解码且长度足够,视为编码后的文件内容 trimmed := strings.TrimSpace(s) if len(trimmed) >= 64 { if b, err := base64.StdEncoding.DecodeString(trimmed); err == nil && len(b) > 0 { return false, "", b, ".png" } } // 文本:以 .inc 存储 return false, "", []byte(s), ".inc" } // isFileURL 判断字符串是否已是对外可访问的 URL 路径(http/https 开头) func isFileURL(s string) bool { lower := strings.ToLower(s) return strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") } // extOfMime 按 MIME 类型推断文件扩展名 func extOfMime(mime string) string { switch strings.ToLower(strings.TrimSpace(mime)) { case "image/png", "png": return ".png" case "image/jpeg", "image/jpg", "jpeg", "jpg": return ".jpg" case "image/webp": return ".webp" case "image/gif": return ".gif" case "audio/mpeg", "audio/mp3", "mp3": return ".mp3" case "audio/wav", "wav": return ".wav" case "video/mp4", "mp4": return ".mp4" case "application/json", "json": return ".json" default: return "" } } // parseDataURI 解析 data URI:data:;base64,,返回解码字节与 mime func parseDataURI(s string) ([]byte, string, bool) { const prefix = "data:" if !strings.HasPrefix(s, prefix) { return nil, "", false } rest := s[len(prefix):] comma := strings.Index(rest, ",") if comma < 0 { return nil, "", false } mime := rest[:comma] if semicolon := strings.Index(mime, ";"); semicolon >= 0 { mime = mime[:semicolon] } payload := strings.TrimPrefix(rest[comma+1:], "base64,") b, err := base64.StdEncoding.DecodeString(payload) if err != nil { return nil, "", false } return b, mime, true }