- 新增计费模块:执行开始建单、终态结算/取消/失败处理,支持按条/按秒/按token计费 - 新增执行生命周期跟踪:优雅关停时取消运行中执行并等待落库 - 新增异步任务等待/通知机制(Wait/Notify) - 重构执行记录落库与进度上报,统一失败分类与重试语义 - 重命名文件:async_task.go→async.go、flow_checkpoint_store.go→exec_checkpoint.go、flow_graph_util.go→exec_record.go - 更新 .gitignore 与数据库密码配置
114 lines
3.2 KiB
Go
114 lines
3.2 KiB
Go
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:<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 URI:data:<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 URI:data:<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
|
||
}
|