Files
ai-agent/workflow/service/flow/lambda_savefile.go
T
19904408334 d699f7ce14 feat(workflow): 增加工作流计费与执行生命周期管理
- 新增计费模块:执行开始建单、终态结算/取消/失败处理,支持按条/按秒/按token计费
- 新增执行生命周期跟踪:优雅关停时取消运行中执行并等待落库
- 新增异步任务等待/通知机制(Wait/Notify)
- 重构执行记录落库与进度上报,统一失败分类与重试语义
- 重命名文件:async_task.go→async.go、flow_checkpoint_store.go→exec_checkpoint.go、flow_graph_util.go→exec_record.go
- 更新 .gitignore 与数据库密码配置
2026-09-03 13:22:22 +08:00

114 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 URIdata:<mime>;base64,<data>)→ 解码为字节,扩展名按 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 URIdata:<mime>;base64,<payload>
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 URIdata:<mime>;base64,<payload>,返回解码字节与 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
}