Author SHA1 Message Date
19904408334 f96d3362ac fix: 修正流程执行列表查询过滤逻辑
Deploy ai-agent to dev k3s / build-and-deploy (push) Failing after 2m24s
2026-08-17 09:57:43 +08:00
19904408334 89326b30ae fix: 修复网关任务并发等待竞态导致永久阻塞
Deploy ai-agent to dev k3s / build-and-deploy (push) Failing after 3m28s
2026-08-14 16:35:44 +08:00
admin 4c940c6753 gitea,gitea-runner写入docker-compose
Deploy ai-agent to dev k3s / build-and-deploy (push) Failing after 3m59s
2026-08-06 09:40:32 +08:00
admin ad9824aa55 k3s脚本
Deploy ai-agent to dev k3s / build-and-deploy (push) Successful in 3m58s
2026-08-03 16:29:47 +08:00
admin 8d7e260da0 gitea,gitea-runner写入docker-compose
Deploy ai-agent to dev k3s / build-and-deploy (push) Failing after 16m30s
2026-08-03 16:10:51 +08:00
19904408334 f8a28bf0f3 fix: 修复编号乱序及日期格式问题
先将日期倒序排序并按展示顺序统计各类型总数,确保倒序编号与前端展示一致;同时修正日期格式为 Y-m-d。
2026-07-22 15:38:48 +08:00
19904408334 ef8388f66b fix: 修正标签编号为倒序排列 2026-07-22 15:26:46 +08:00
19904408334 d2eef8d3ca Merge branch '2026-06-16' of http://116.204.74.41:3000/red-future/ai-agent into 2026-06-16 2026-07-22 15:17:05 +08:00
19904408334 031819f634 fix: 修复输出项编号为全局连续编号及日期格式 2026-07-22 15:16:41 +08:00
admin 20723f58ce ng代理minio支持websocket 2026-07-21 17:40:41 +08:00
19904408334 08251d9a73 fix: 修复并发取消逻辑与HTTP请求超时及响应解析 2026-07-09 13:44:13 +08:00
19904408334 cc29dd21e4 feat: 添加 ModelOutputFields 字段 2026-07-08 13:35:50 +08:00
19904408334 14c88efe79 feat: 新增会话与结果管理功能及扩展字段
重构执行列表树状结构,扁平化日期节点下的输出项,新增会话列表查询、结果删除与软删除标记,引入扩展字段与模板配置,升级依赖版本。
2026-07-08 09:22:54 +08:00
19904408334 4ccb81b34a feat: 添加表单视频参数校验 2026-07-04 16:39:59 +08:00
19904408334 e548f20b6e feat: 添加执行费用统计及token信息记录
在流程执行和节点执行中新增 TotalFee 和 TokenInfo 字段,用于记录每次模型调用的详细计费数据;流程执行完成时汇总所有节点的 token 消耗和费用;重构 token 更新逻辑,从响应字段提取改为使用回调返回的 billingData;优化 URL 后缀提取方法以正确处理带查询参数的链接。
2026-07-01 19:34:31 +08:00
19904408334 f5be9d8a40 feat: 支持视频模型首帧参数及顺序执行逻辑
添加 FirstFrame 字段到模型信息结构体,重构视频节点数据转换处理,支持从上游节点获取视频配置并区分输出字段。实现视频结果重新上传至 OSS,修正字段匹配逻辑为精确匹配,并基于 return_last_frame 参数优化顺序执行中的首帧与内容传递。
2026-06-30 15:15:02 +08:00
19904408334 b83b18a5c8 fix: 修复字幕分段逻辑及输出规范拼接
重写 splitTextByPunct 函数,使用正则索引切分并保留标点,避免原 Split 方法产生空串及奇偶位错位问题。优化 BuildSubtitles 中分段与 Word 的匹配逻辑,使用 strings.Builder 提升拼接性能,并修正字幕文本剔除标点。调整输出字段的 Prompt 拼接格式,改为直接输出模板结构规范。
2026-06-29 13:11:54 +08:00
admin fa2a07600b common版本更新 2026-06-25 17:05:14 +08:00
19904408334 0c50178f0a fix: 修复字幕节点数据转换逻辑 2026-06-25 14:10:21 +08:00
19904408334 8209219f92 fix: 修复单视频生成时无需拼接的逻辑 2026-06-25 10:54:40 +08:00
19904408334 48f3b46929 Merge branch '2026-06-16' of http://116.204.74.41:3000/red-future/ai-agent into 2026-06-16 2026-06-24 16:39:26 +08:00
19904408334 c19bd17632 fix: 修复遍历中删除map键及替换视频时长字段 2026-06-24 16:38:43 +08:00
admin bf5d88158e common版本回退 2026-06-24 10:56:52 +08:00
admin 8a639d3d09 common版本升级 2026-06-23 18:32:01 +08:00
admin d6e6d0b403 common版本更新 2026-06-23 16:56:33 +08:00
19904408334 ffba7ff726 chore: 升级依赖至最新版本 2026-06-23 16:54:39 +08:00
19904408334 6942e05f1a fix: 补充Lambda节点HTTP请求空结果错误提示 2026-06-23 16:40:42 +08:00
19904408334 36f9871deb fix: 替换CDN前缀为minio配置
修复视频URL拼接时硬编码CDN地址的问题,改为从配置中动态读取minio前缀。
2026-06-23 11:00:22 +08:00
19904408334 91a6436ad7 fix: 替换CDN前缀为minio配置
修复视频URL拼接时硬编码CDN地址的问题,改为从配置中动态读取minio前缀。
2026-06-23 10:42:58 +08:00
19904408334 e4bd08458a fix: 区分CDN与Minio内网地址配置 2026-06-23 08:59:02 +08:00
admin 243c9e291f 封版 2026-06-22 11:47:37 +08:00
admin 84592a09df 封版 2026-06-22 11:40:03 +08:00
19904408334 44702d32ad build: 升级 common 依赖至 v0.0.24 2026-06-22 11:36:52 +08:00
19904408334 29557bdc4d feat: 将用户提示词作为独立字段传递
重构用户提示词的传递方式,将其从嵌入到 userForm 中改为 ComposeCallbackReq 的独立字段 UserPrompt
2026-06-18 17:25:58 +08:00
19904408334 4df45069e0 feat: 重构节点上下文与并发执行逻辑
重构GetNodeContextContent返回类型为切片,修复并发竞态与协程泄漏问题;回调改用OSS文件获取结果;调整节点输入上传时序
2026-06-18 14:24:48 +08:00
22 changed files with 1106 additions and 499 deletions
+1
View File
@@ -0,0 +1 @@
.git
+37
View File
@@ -0,0 +1,37 @@
# ============================================================
# ai-agent dev 分支自动发布
# 触发:push dev 分支
# 流程:构建镜像 -> push 本地仓库(127.0.0.1:5000) -> 部署到 dev 宿主机本地 k3s
# 前置:runner 挂载 dev k3s kubeconfig 到 /root/.kube/k3s-dev.yaml
# ============================================================
name: Deploy ai-agent to dev k3s
on:
push:
branches: [ 2026-06-16 ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
# 不依赖 actions/checkout(从 github.com 拉 action 源码,国内网络不稳定),
# 直接 clone 本地 Gitea,用 Actions 注入的 GITHUB_TOKEN 认证
- name: 拉取代码
run: |
git clone --branch dev http://x-access-token:${GITHUB_TOKEN}@127.0.0.1:3000/red-future/ai-agent.git .
git checkout ${GITHUB_SHA}
- name: 构建并推送镜像
run: |
TAG=dev-${GITHUB_SHA::8}
docker build -t 127.0.0.1:5000/ai-agent:${TAG} .
docker push 127.0.0.1:5000/ai-agent:${TAG}
- name: 部署到 dev k3s
env:
KUBECONFIG: /root/.kube/k3s-dev.yaml
run: |
kubectl create namespace dev --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f deploy/k8s/ai-agent.yaml
kubectl set image deployment/ai-agent ai-agent=127.0.0.1:5000/ai-agent:dev-${GITHUB_SHA::8} -n dev
kubectl rollout status deployment/ai-agent -n dev --timeout=5m
+2 -26
View File
@@ -1,7 +1,8 @@
# 阶段1: 构建
FROM golang:alpine AS builder
RUN apk add --no-cache git ca-certificates tzdata
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
apk add --no-cache git ca-certificates tzdata
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
@@ -10,12 +11,6 @@ ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.cn,direct
ENV CGO_ENABLED=0
ENV GOTOOLCHAIN=auto
ENV GOPRIVATE=gitea.com/red-future/common
# 配置git使用私有Gitea仓库
RUN git config --global url."http://x-token-auth:9b31146aa8c10a7cb4f2e49dcee0934a223be1076289810e1ad98b968066c2bc@116.204.74.41:3000/red-future/common.git".insteadOf "https://gitea.com/red-future/common.git" && \
git config --global credential.helper store
WORKDIR /build
COPY . .
@@ -24,25 +19,6 @@ RUN go mod download && go mod tidy
RUN go build -ldflags="-s -w" -o main ./main.go
# 阶段2: 运行
FROM alpine:3.19
RUN apk add --no-cache ca-certificates tzdata
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
WORKDIR /app
COPY --from=builder /build/main .
COPY --from=builder /build/config.yml ./
RUN mkdir -p /app/resource/log/run \
/app/resource/log/server \
&& adduser -D -u 1000 appuser \
&& chown -R appuser:appuser /app
USER appuser
EXPOSE 3005
+9 -9
View File
@@ -5,7 +5,7 @@ server:
database:
default:
- type: "pgsql"
host: "116.204.74.41"
host: "192.168.0.83"
port: "15432"
user: "postgres"
pass: "Bjang09@686^*^"
@@ -26,7 +26,7 @@ database:
timeMaintainDisabled: false # (可选)是否完全关闭时间更新特性,为true时CreatedAt/UpdatedAt/DeletedAt都将失效
black_deacon:
- type: "pgsql"
host: "116.204.74.41"
host: "192.168.0.83"
port: "15432"
user: "postgres"
pass: "Bjang09@686^*^"
@@ -48,7 +48,7 @@ database:
redis:
default:
address: 116.204.74.41:6379
address: 192.168.0.83:6379
db: 0
idleTimeout: "60s" #连接最大空闲时间,使用时间字符串例如30s/1m/1d
maxConnLifetime: "90s" #连接最长存活时间,使用时间字符串例如30s/1m/1d
@@ -59,13 +59,13 @@ redis:
maxActive: 100
consul:
address: 116.204.74.41:8500
address: 192.168.0.83:8500
jaeger:
addr: 116.204.74.41:4318
addr: 192.168.0.83:4318
# 文件上传服务地址,与oss模块minio中的endpoint一致
filePrefix: "http://116.204.74.41:9000"
# 文件上传服务地址,cdn访问地址
filePrefix: "http://cdn.redpowerfuture.com"
model-asynch:
addr: "127.0.0.1:8001"
# 文件上传服务地址,minio内网访问地址
minioPrefix: "http://192.168.0.83:9000"
+46
View File
@@ -0,0 +1,46 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ai-agent
namespace: dev
spec:
replicas: 1
selector:
matchLabels:
app: ai-agent
template:
metadata:
labels:
app: ai-agent
spec:
containers:
- name: ai-agent
image: 127.0.0.1:5000/ai-agent:dev-latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3005
env:
- name: TZ
value: Asia/Shanghai
readinessProbe:
tcpSocket:
port: 3005
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 3005
initialDelaySeconds: 30
periodSeconds: 15
---
apiVersion: v1
kind: Service
metadata:
name: ai-agent
namespace: dev
spec:
selector:
app: ai-agent
ports:
- port: 3005
targetPort: 3005
+9 -11
View File
@@ -3,8 +3,8 @@ module ai-agent
go 1.26.0
require (
gitea.redpowerfuture.com/red-future/common v0.0.23
github.com/cloudwego/eino v0.9.5
gitea.redpowerfuture.com/red-future/common v0.0.29
github.com/cloudwego/eino v0.9.12
github.com/cloudwego/eino-ext/components/model/qwen v0.1.9
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.2
@@ -29,7 +29,6 @@ require (
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgraph-io/badger/v4 v4.2.0 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
@@ -66,7 +65,7 @@ require (
github.com/hashicorp/serf v0.10.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
@@ -82,16 +81,15 @@ require (
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/olekukonko/tablewriter v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.2-0.20201214064552-5dd12d0cfe7f // indirect
github.com/r3labs/diff/v2 v2.15.1 // indirect
github.com/redis/go-redis/v9 v9.12.1 // indirect
github.com/redis/go-redis/v9 v9.17.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tiger1103/gfast-token v1.0.10 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/vcaesar/cedar v0.30.0 // indirect
@@ -107,8 +105,8 @@ require (
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
golang.org/x/arch v0.11.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/arch v0.19.0 // indirect
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
+20 -36
View File
@@ -1,8 +1,6 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
gitea.com/red-future/common v0.0.21 h1:8w30HmCVmFG/hphH3ODJs1KxDEGmRpq+/PXI0pQjJKc=
gitea.com/red-future/common v0.0.21/go.mod h1:6/nqIucVzmjOyqDTIq71feYBXXFNBy0rFwzaQ0/Ueoo=
gitea.redpowerfuture.com/red-future/common v0.0.23 h1:xieoA00iKOCDm5SO9iXn+cSyMKBAlZwI0fuEVPWrHLg=
gitea.redpowerfuture.com/red-future/common v0.0.23/go.mod h1:50U1Xi+Ie56z09S5LQbZvaken0Mxv3OeS9LgR7U/ZRY=
gitea.redpowerfuture.com/red-future/common v0.0.29 h1:5McaN5pSewvrLUHQzWMX6EaUvD+B5I5bMYoU+clHJk4=
gitea.redpowerfuture.com/red-future/common v0.0.29/go.mod h1:50U1Xi+Ie56z09S5LQbZvaken0Mxv3OeS9LgR7U/ZRY=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
@@ -58,10 +56,8 @@ github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/eino v0.8.13 h1:z5dhaZNN8TWZbP/lgKxGmF26Ii8fPeUlQCGV/NTtms0=
github.com/cloudwego/eino v0.8.13/go.mod h1:+2N4nsMPxA6kGBHpH+75JuTfEcGprAMTdsZESrShKpU=
github.com/cloudwego/eino v0.9.5 h1:0Nftjx9gPek/2S/hzm38LVxSjk5/6mqRr3I9VKrKvm4=
github.com/cloudwego/eino v0.9.5/go.mod h1:OBD1mrkfkt/pJa4rkg1P0VnaMeOVl7l8IAdEqY//3IQ=
github.com/cloudwego/eino v0.9.12 h1:mHAMo5k7GdvnVD8Lc2sLyfpkxEm0S/y3PkEMhsSYt78=
github.com/cloudwego/eino v0.9.12/go.mod h1:OBD1mrkfkt/pJa4rkg1P0VnaMeOVl7l8IAdEqY//3IQ=
github.com/cloudwego/eino-ext/components/model/qwen v0.1.9 h1:xCz/mp43JeWqupjPR3zLRArmwC6P29/6lTwbwh1yzYM=
github.com/cloudwego/eino-ext/components/model/qwen v0.1.9/go.mod h1:slTGTuhzkzhNavf+1UtUg1FvUSA31iNAF+rq1mT4SnI=
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17 h1:EeVcR1TslRA2IdNW1h/2LaGbPlffwGhQm99jM3zWZiI=
@@ -116,20 +112,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0 h1:39+jbTenm7KBj4hO2C8ANAxVHpX/7OuRDs1VcGC9ylA=
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0/go.mod h1:B0s0fVzn0W220E8UTpSGzrrGKsop5KcB90twBeLCiz0=
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2 h1:u8EpP24GkprogROnJ7htMov9Fc66pTP1eVYrWxiCYOs=
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2/go.mod h1:GmvM3r8GVByVMi4RD2+MCs5+CfxVXPMeT8mVDkAaAXE=
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0 h1:N/F9CuDdUZLoM1nVRqrDE/33pDZuhVxpNY4wYdeIaBs=
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0/go.mod h1:x6uoJGfZOtirIRQls8xUlYzC6f7T/eULPUa9er368X0=
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.2 h1:iTQegT+lEg/wDKvj2mi3W1wrdrwFarjokf88EXVVgu4=
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.2/go.mod h1:ZRw3GNz5cq4uYrW4TPSVyrYWaoqzujKdWro/AOcGBaE=
github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5 h1:eUqwJ/qNH8lJ6yssiqskazgp1ACQuNU6zXlLOZVuXTQ=
github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5/go.mod h1:sjQyMry9+0POYZCA6lHXBxO77WoNKkruJpRB4xKqk5k=
github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5 h1:tHUEZYB5GTqEYYVDYnlGobf1xISARKDE4KHVlgjwTec=
github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5/go.mod h1:cfzTn2HS9RDX8f5pUVkbGxUWcSosouqfNQ1G6cY0V88=
github.com/gogf/gf/v2 v2.10.0 h1:rzDROlyqGMe/eM6dCalSR8dZOuMIdLhmxKSH1DGhbFs=
github.com/gogf/gf/v2 v2.10.0/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
github.com/gogf/gf/v2 v2.10.2 h1:46IO0Uc8e85/FqdftJFskfDejJLBL0JBnGS5qOftUu8=
github.com/gogf/gf/v2 v2.10.2/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -243,8 +233,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -314,12 +304,13 @@ github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.2-0.20201214064552-5dd12d0cfe7f h1:lJqhwddJVYAkyp72a4pwzMClI20xTwL7miDdm2W/KBM=
github.com/pkg/errors v0.9.2-0.20201214064552-5dd12d0cfe7f/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -339,13 +330,13 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/r3labs/diff/v2 v2.15.1 h1:EOrVqPUzi+njlumoqJwiS/TgGgmZo83619FNDB9xQUg=
github.com/r3labs/diff/v2 v2.15.1/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85tWFM9kc=
github.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg=
github.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
@@ -384,8 +375,9 @@ github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tiger1103/gfast-token v1.0.10 h1:fNiBE/Dq5iTHvTGlCx3DmXa2o4hr0NtumFpffZ39k6s=
@@ -411,28 +403,20 @@ go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
@@ -441,8 +425,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU=
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
@@ -451,8 +435,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+1
View File
@@ -106,6 +106,7 @@ type NodeItem struct {
NodeCode NodeType `json:"nodeCode"`
ModelType int `json:"modelType"`
NodeName string `json:"nodeName"` // 从常量来
PatchLayout bool `json:"patchLayout"`
SkillOption bool `json:"skillOption"`
PromptOption bool `json:"promptOption"`
IsSaveFile bool `json:"isSaveFile"`
@@ -38,3 +38,17 @@ func (c *flowExecution) Get(ctx context.Context, req *flowDto.GetFlowExecutionRe
func (c *flowExecution) List(ctx context.Context, req *flowDto.ListFlowExecutionReq) (res *flowDto.ListFlowExecutionTreeRes, err error) {
return flowService.FlowExecutionService.List(ctx, req)
}
func (c *flowExecution) GetSessionList(ctx context.Context, req *flowDto.GetSessionListReq) (res *flowDto.ListFlowExecutionRes, err error) {
return flowService.FlowExecutionService.GetSessionList(ctx, req)
}
func (c *flowExecution) DeleteResult(ctx context.Context, req *flowDto.DeleteResultReq) (res *beans.ResponseEmpty, err error) {
err = flowService.FlowExecutionService.DeleteResult(ctx, req)
return
}
func (c *flowExecution) DeleteSession(ctx context.Context, req *flowDto.DeleteSessionReq) (res *beans.ResponseEmpty, err error) {
err = flowService.FlowExecutionService.DeleteSession(ctx, req)
return
}
+11 -1
View File
@@ -7,6 +7,7 @@ import (
"context"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
@@ -49,8 +50,17 @@ func (d *flowExecutionDao) Get(ctx context.Context, req *flowDto.GetFlowExecutio
}
func (d *flowExecutionDao) List(ctx context.Context, req *flowDto.ListFlowExecutionReq, fields ...string) (res []*entity.FlowExecution, total int, err error) {
model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowExecution).Fields(fields).OmitEmpty()
model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowExecution).Fields(fields)
model.Where(entity.FlowExecutionCol.Creator, req.Creator)
if !g.IsEmpty(req.ResultDel) {
model.Where(entity.FlowExecutionCol.ResultDel, false)
}
if !g.IsEmpty(req.SessionDel) {
model.Where(entity.FlowExecutionCol.SessionDel, false)
}
if req.IsResult {
model.WhereNot(entity.FlowExecutionCol.OutputParams, "[]")
}
model.OrderDesc(entity.FlowExecutionCol.CreatedAt)
if req.Page != nil {
model.Page(int(req.Page.PageNum), int(req.Page.PageSize))
+1
View File
@@ -86,6 +86,7 @@ func (d *nodeExecutionDao) Get(ctx context.Context, req *nodeDto.GetNodeExecutio
func (d *nodeExecutionDao) ListByFlowExecutionId(ctx context.Context, req *nodeDto.ListNodeExecutionByFlowReq, fields ...string) (res []*entity.NodeExecution, total int, err error) {
model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameNodeExecution).NoTenantId(ctx).Fields(fields).OmitEmpty()
model.Where(entity.NodeExecutionCol.FlowExecutionId, req.FlowExecutionId)
model.Where(entity.NodeExecutionCol.NodeGroupId, req.NodeGroupId)
model.OrderAsc(entity.NodeExecutionCol.CreatedAt)
if req.Page != nil {
model.Page(int(req.Page.PageNum), int(req.Page.PageSize))
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"fmt"
"strings"
"gitea.com/red-future/common/db/gfdb"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/util/gconv"
)
+58 -27
View File
@@ -30,6 +30,7 @@ type FlowExecutionInput struct {
ExecutionId int64 `json:"executionId"`
ConfigMap map[string]*entity.FlowNode `json:"configMap"`
SessionId string `json:"sessionId" dc:"会话ID"`
Templates []map[string]any `json:"templates"`
Desc string `json:"desc"`
SkillName string `json:"skillName"`
FileUrl []string `json:"fileUrl"`
@@ -49,6 +50,7 @@ type GetModelInfoReq struct {
type GetModelInfoRes struct {
Model struct {
FirstFrame string `json:"firstFrame"`
LastFrame string `json:"lastFrame"`
ResponseTokenField string `json:"responseTokenField"`
ResponseMapping map[string]any `json:"responseMapping"`
@@ -74,6 +76,7 @@ type ComposeMessagesReq struct {
CallbackUrl string `json:"callbackUrl"`
Form []map[string]any `json:"form"`
UserForm []map[string]any `json:"userForm"`
UserPrompt string `json:"userPrompt" dc:"用户提示词"`
Consult []Consult `json:"consult"`
SessionId string `json:"sessionId" dc:"会话ID"`
NodeId string `json:"nodeId"`
@@ -122,18 +125,19 @@ type ComposeCallbackReq struct {
TotalRounds int `json:"total_rounds"` // 总轮数
Rounds []map[string]any `json:"rounds"` // 每轮详情(动态类型)
} `json:"messages,omitempty"`
EpicycleId int64 `json:"epicycleId"`
ErrorMsg string `json:"errorMsg,omitempty"`
EpicycleId int64 `json:"epicycleId"`
ErrorMsg string `json:"errorMsg,omitempty"`
BillingData []map[string]any `json:"billing_data"`
}
type ModelCallbackReq struct {
g.Meta `path:"/modelCallback" method:"post" tags:"提示词处理" summary:"model-gateway 回调" dc:"model-gateway 成功后 GET 回调:callbackUrl/{bizName}"`
TaskId string `p:"task_id" json:"task_id" v:"required#task_id不能为空" dc:"网关任务ID"`
State int `p:"state" json:"state" dc:"网关任务状态"`
OssFile string `p:"oss_file" json:"oss_file" dc:"结果文件地址"`
FileType string `p:"file_type" json:"file_type" dc:"结果文件类型"`
Messages map[string]any `json:"messages"`
ErrorMsg string `json:"error_msg"`
g.Meta `path:"/modelCallback" method:"post" tags:"提示词处理" summary:"model-gateway 回调" dc:"model-gateway 成功后 GET 回调:callbackUrl/{bizName}"`
TaskId string `p:"task_id" json:"task_id" v:"required#task_id不能为空" dc:"网关任务ID"`
State int `p:"state" json:"state" dc:"网关任务状态"`
OssFile string `p:"oss_file" json:"oss_file" dc:"结果文件地址"`
FileType string `p:"file_type" json:"file_type" dc:"结果文件类型"`
ErrorMsg string `json:"error_msg"`
BillingData []map[string]any `json:"billing_data"`
}
type VideoCallbackReq struct {
@@ -181,6 +185,7 @@ type ExecuteReq struct {
FlowContent *entity.FlowInfo `json:"flowContent" description:"流程内容"`
NodeInputParams []*entity.FlowNode `json:"nodeInputParams" description:"节点输入参数"`
SessionId string `json:"sessionId" dc:"会话ID"`
Templates []map[string]any `json:"templates"`
Desc string `json:"desc"`
SkillName string `json:"skillName"`
FileUrl []string `json:"fileUrl"`
@@ -210,6 +215,7 @@ type CreateFlowExecutionReq struct {
ErrorMessage string `json:"errorMessage" description:"错误信息"`
TraceId string `json:"traceId" description:"跟踪ID"`
SessionId string `json:"sessionId" dc:"会话ID"`
Extension map[string]interface{} `json:"extension"`
}
type CreateFlowExecutionRes struct {
@@ -217,13 +223,33 @@ type CreateFlowExecutionRes struct {
}
type UpdateFlowExecutionReq struct {
Id int64 `json:"id" v:"required#ID不能为空"`
NodeGroupId string `json:"nodeGroupId"`
DurationMs int64 `json:"durationMs" description:"执行时长(毫秒)"`
Status flow.FlowExecutionStatus `json:"status" description:"状态:1-运行中,2-成功,3-失败"`
OutputParams []map[string]interface{} `json:"outputParams" description:"输出参数"`
ErrorMessage string `json:"errorMessage" description:"错误信息"`
TraceId string `json:"traceId" description:"跟踪ID"`
Id int64 `json:"id" v:"required#ID不能为空"`
NodeGroupId string `json:"nodeGroupId"`
DurationMs int64 `json:"durationMs" description:"执行时长(毫秒)"`
Status flow.FlowExecutionStatus `json:"status" description:"状态:1-运行中,2-成功,3-失败"`
FlowContent *entity.FlowInfo `json:"flowContent" description:"流程内容"`
NodeInputParams []*entity.FlowNode `json:"nodeInputParams" description:"节点输入参数"`
OutputParams []map[string]interface{} `json:"outputParams" description:"输出参数"`
ErrorMessage string `json:"errorMessage" description:"错误信息"`
TraceId string `json:"traceId" description:"跟踪ID"`
TotalTokens int `json:"totalTokens" description:"总token"`
TotalFee float64 `json:"totalFee" description:"总费用"`
SessionDel bool `json:"sessionDel" description:"会话是否删除"`
ResultDel bool `json:"resultDel" description:"结果是否删除"`
Extension map[string]interface{} `json:"extension"`
}
type DeleteResultReq struct {
g.Meta `path:"/deleteResult" method:"delete" tags:"任务管理" summary:"删除结果" dc:"删除结果"`
Id int64 `json:"id" v:"required#ID不能为空"`
Content string `json:"content"`
}
type DeleteSessionReq struct {
g.Meta `path:"/deleteSession" method:"delete" tags:"任务管理" summary:"删除会话" dc:"删除会话"`
Id int64 `json:"id" v:"required#ID不能为空"`
}
type GetFlowExecutionReq struct {
@@ -233,11 +259,20 @@ type GetFlowExecutionReq struct {
SessionId string `json:"sessionId" dc:"会话ID"`
}
type GetSessionListReq struct {
g.Meta `path:"/sessionList" method:"get" tags:"任务管理" summary:"会话列表" dc:"会话列表"`
*beans.Page `json:"page"`
}
type ListFlowExecutionReq struct {
g.Meta `path:"/list" method:"get" tags:"任务管理" summary:"任务列表" dc:"任务列表"`
Page *beans.Page `json:"page"`
Creator string `json:"creator"`
*beans.Page `json:"page"`
Creator string `json:"creator"`
SessionDel *bool `json:"sessionDel"`
ResultDel *bool `json:"resultDel"`
IsResult bool `json:"isResult"`
}
type ListFlowExecutionRes struct {
@@ -256,6 +291,7 @@ type VOFlowExecution struct {
NodeInputParams []*entity.FlowNode `json:"nodeInputParams" description:"节点输入参数"`
OutputParams []map[string]interface{} `json:"outputParams" description:"输出参数"`
ErrorMessage string `json:"errorMessage" description:"错误信息"`
Extension map[string]interface{} `json:"extension"`
TraceId string `json:"traceId" description:"跟踪ID"`
SessionId string `json:"sessionId" dc:"会话ID"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
@@ -264,22 +300,17 @@ type VOFlowExecution struct {
}
// ========== 核心:构建树状结构 ==========
// 定义树结构
type OutputItem struct {
Id int64 `json:"id,string" description:"ID"`
Timestamp string `json:"timestamp" description:"时间戳key"`
Content string `json:"content" description:"内容值"`
Type string `json:"type" description:"类型"`
Label string `json:"label" description:"后缀+数字标号"`
}
type FlowNode struct {
FlowName string `json:"flowName" description:"流程名称"`
Id int64 `json:"Id,string" description:"任务ID"`
SessionId string `json:"sessionId" description:"会话ID"`
Items []OutputItem `json:"items" description:"输出项列表"`
}
type DateNode struct {
CreateDate string `json:"createDate" description:"创建日期"`
Flows []FlowNode `json:"flows" description:"流程列表"`
CreateDate string `json:"createDate" description:"创建日期"`
Items []OutputItem `json:"items" description:"直接是结果项列表"`
}
type ListFlowExecutionTreeRes struct {
@@ -38,6 +38,7 @@ type UpdateNodeExecutionReq struct {
PromptTokens int `json:"promptTokens"`
CompletionTokens int `json:"completionTokens"`
TotalTokens int `json:"totalTokens"`
TokenInfo []map[string]any `json:"tokenInfo"`
Status node.NodeExecutionStatus `json:"status"`
DurationMs int64 `json:"durationMs"`
ErrorMessage string `json:"errorMessage"`
@@ -60,6 +61,7 @@ type ListNodeExecutionByFlowReq struct {
g.Meta `path:"/listByFlow" method:"get" tags:"节点执行记录" summary:"查询流程节点执行列表" dc:"查询指定流程执行下的所有节点执行记录"`
Page *beans.Page `json:"page"`
FlowExecutionId int64 `json:"flowExecutionId" v:"required#流程执行ID不能为空"`
NodeGroupId string `json:"nodeGroupId"`
}
// NodeExecutionResp 节点执行记录响应
+12
View File
@@ -22,6 +22,10 @@ type FlowExecution struct {
TraceId string `orm:"trace_id" json:"traceId" description:"跟踪ID"`
SessionId string `orm:"session_id" json:"sessionId" description:"会话ID"`
TotalTokens int `orm:"total_tokens" json:"totalTokens" description:"总token消耗"`
TotalFee int `orm:"total_fee" json:"totalFee" description:"总费用"`
SessionDel bool `orm:"session_del" json:"sessionDel" description:"会话是否删除"`
ResultDel bool `orm:"result_del" json:"resultDel" description:"结果是否删除"`
Extension map[string]interface{} `orm:"extension" json:"extension" description:"扩展字段"`
}
type flowExecutionCol struct {
@@ -39,6 +43,10 @@ type flowExecutionCol struct {
TraceId string
SessionId string
TotalTokens string
TotalFee string
SessionDel string
ResultDel string
Extension string
}
var FlowExecutionCol = flowExecutionCol{
@@ -56,4 +64,8 @@ var FlowExecutionCol = flowExecutionCol{
TraceId: "trace_id",
SessionId: "session_id",
TotalTokens: "total_tokens",
TotalFee: "total_fee",
SessionDel: "session_del",
ResultDel: "result_del",
Extension: "extension",
}
+14 -12
View File
@@ -15,18 +15,20 @@ type FlowInfo struct {
}
type FlowNode struct {
Id string `json:"id"`
NodeCode node.NodeType `json:"nodeCode"`
Name string `json:"name"`
Config map[string]interface{} `json:"config"`
SkillName string `json:"skillName"`
PromptContent string `json:"promptContent"`
IsSaveFile bool `json:"isSaveFile"`
InputSource []FlowNodeInputSource `json:"inputSource"` // 前端指定:来源节点ID
FormConfig []node.NodeFormField `json:"formConfig"`
ModelConfig node.ModelItem `json:"modelConfig"`
OutputConfig []node.NodeFormField `json:"outputConfig"`
OutputResult []node.NodeFormField `json:"outputResult" ds:"节点输出结果"`
Id string `json:"id"`
NodeCode node.NodeType `json:"nodeCode"`
Name string `json:"name"`
Config map[string]interface{} `json:"config"`
PatchLayout bool `json:"patchLayout"`
SkillName string `json:"skillName"`
PromptContent string `json:"promptContent"`
IsSaveFile bool `json:"isSaveFile"`
InputSource []FlowNodeInputSource `json:"inputSource"` // 前端指定:来源节点ID
FormConfig []node.NodeFormField `json:"formConfig"`
ModelConfig node.ModelItem `json:"modelConfig"`
ModelOutputFields []string `json:"modelOutputFields"`
OutputConfig []node.NodeFormField `json:"outputConfig"`
OutputResult []node.NodeFormField `json:"outputResult" ds:"节点输出结果"`
}
type FlowNodeInputSource struct {
+3
View File
@@ -22,6 +22,7 @@ type NodeExecution struct {
PromptTokens int `orm:"prompt_tokens" json:"promptTokens" description:"提示词token消耗"`
CompletionTokens int `orm:"completion_tokens" json:"completionTokens" description:"补全token消耗"`
TotalTokens int `orm:"total_tokens" json:"totalTokens" description:"总token消耗"`
TokenInfo []map[string]interface{} `orm:"token_info" json:"tokenInfo" description:"token信息"`
Status node.NodeExecutionStatus `orm:"status" json:"status" description:"执行状态:1-运行中,2-成功,3-失败,4-暂停,5-等待执行"`
DurationMs int64 `orm:"duration_ms" json:"durationMs" description:"执行时长(毫秒)"`
ErrorMessage string `orm:"error_message" json:"errorMessage" description:"错误信息"`
@@ -40,6 +41,7 @@ type nodeExecutionCol struct {
PromptTokens string
CompletionTokens string
TotalTokens string
TokenInfo string
Status string
DurationMs string
ErrorMessage string
@@ -58,6 +60,7 @@ var NodeExecutionCol = nodeExecutionCol{
PromptTokens: "prompt_tokens",
CompletionTokens: "completion_tokens",
TotalTokens: "total_tokens",
TokenInfo: "token_info",
Status: "status",
DurationMs: "duration_ms",
ErrorMessage: "error_message",
+195 -128
View File
@@ -16,14 +16,12 @@ import (
"fmt"
"sort"
"strconv"
"strings"
"sync"
"time"
"gitea.redpowerfuture.com/red-future/common/utils"
"github.com/cloudwego/eino/compose"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
"github.com/google/uuid"
"go.opentelemetry.io/otel/trace"
@@ -47,54 +45,94 @@ func (s *flowExecutionService) Get(ctx context.Context, req *flowDto.GetFlowExec
return res, err
}
func (s *flowExecutionService) DeleteResult(ctx context.Context, req *flowDto.DeleteResultReq) (err error) {
r, err := flowDao.FlowExecutionDao.Get(ctx, &flowDto.GetFlowExecutionReq{Id: req.Id})
if err != nil {
return err
}
// 遍历并剔除值等于 req.Url 的数据
newOutputParams := make([]map[string]any, 0)
for _, paramMap := range r.OutputParams {
// 单个 paramMap 过滤
cleanMap := make(map[string]any)
for k, v := range paramMap {
// 转为字符串对比
if gconv.String(v) != req.Content {
cleanMap[k] = v
}
}
// 只保留非空 map,避免出现空层级
if len(cleanMap) > 0 {
newOutputParams = append(newOutputParams, cleanMap)
}
}
// 赋值回原数据
r.OutputParams = newOutputParams
// 执行更新:更新 OutputParams + 标记删除
flowUpdateReq := new(flowDto.UpdateFlowExecutionReq)
flowUpdateReq.Id = req.Id
flowUpdateReq.OutputParams = r.OutputParams
_, err = flowDao.FlowExecutionDao.Update(ctx, flowUpdateReq)
return
}
func (s *flowExecutionService) DeleteSession(ctx context.Context, req *flowDto.DeleteSessionReq) (err error) {
flowUpdateReq := new(flowDto.UpdateFlowExecutionReq)
flowUpdateReq.Id = req.Id
flowUpdateReq.SessionDel = true
_, err = flowDao.FlowExecutionDao.Update(ctx, flowUpdateReq)
return
}
func (s *flowExecutionService) GetSessionList(ctx context.Context, req *flowDto.GetSessionListReq) (res *flowDto.ListFlowExecutionRes, err error) {
user, err := utils.GetUserInfo(ctx)
if err != nil {
return
}
flowReq := new(flowDto.ListFlowExecutionReq)
flowReq.Page = req.Page
flowReq.Creator = user.UserName
flowReq.SessionDel = gconv.PtrBool(true)
list, total, err := flowDao.FlowExecutionDao.List(ctx, flowReq)
if err != nil {
return nil, err
}
res = &flowDto.ListFlowExecutionRes{
Total: total,
}
err = gconv.Struct(list, &res.List)
return res, err
}
func (s *flowExecutionService) List(ctx context.Context, req *flowDto.ListFlowExecutionReq) (res *flowDto.ListFlowExecutionTreeRes, err error) {
user, err := utils.GetUserInfo(ctx)
if err != nil {
return
}
req.Creator = user.UserName
req.ResultDel = gconv.PtrBool(true)
req.IsResult = true
list, _, err := flowDao.FlowExecutionDao.List(ctx, req)
if err != nil {
return nil, err
}
// ===================== 核心修复:只统计【有数据】的执行记录,空的直接跳过 =====================
executionNumber := make(map[int64]int) // executionId -> 倒序编号(最新=1
// 第一次遍历:只处理【有输出参数】的记录,统计并分配编号
var validList []*entity.FlowExecution // 只存有效(非空)记录
// 过滤出有有效输出的执行记录
var validList []*entity.FlowExecution
for _, execution := range list {
if g.IsEmpty(execution.OutputParams) {
continue // 空数据直接过滤,不参与编号、不展示
if !g.IsEmpty(execution.OutputParams) {
validList = append(validList, execution)
}
validList = append(validList, execution)
}
// 给有效记录分配【时间倒序编号】(最新=1)
totalValid := len(validList)
for idx, execution := range validList {
executionNumber[execution.Id] = totalValid - idx
}
// 2. 分组映射:日期 -> 流程节点
type flowWrap struct {
flowNode flowDto.FlowNode
createdAt *gtime.Time
}
dateMap := make(map[string]*[]flowWrap)
// 遍历【有效数据】构建结构
// 1. 按日期归集,严格使用 Y-m-d 格式
dateMap := make(map[string][]flowDto.OutputItem)
for _, execution := range validList {
// 按要求使用 Y-m-d
createDate := execution.CreatedAt.Format("Y-m-d")
flowName := execution.FlowName
execID := execution.Id
outputParams := execution.OutputParams
// 编号只算有效数据,不会把空的算进去
num := executionNumber[execution.Id]
displayFlowName := fmt.Sprintf("会话-%d(%s)", num, flowName)
// 3. 解析 outputParams
var tempItems []flowDto.OutputItem
for _, paramMap := range outputParams {
for tsKey, value := range paramMap {
@@ -102,98 +140,116 @@ func (s *flowExecutionService) List(ctx context.Context, req *flowDto.ListFlowEx
continue
}
tempItems = append(tempItems, flowDto.OutputItem{
Id: execID,
Timestamp: tsKey,
Content: gconv.String(value),
})
}
}
// ===================== 修复1:如果解析后依然为空,直接跳过,不生成第二层节点 =====================
if len(tempItems) == 0 {
continue
}
// 时间戳正序
// 单条执行内按时间戳正序
sort.Slice(tempItems, func(i, j int) bool {
t1, _ := strconv.ParseInt(tempItems[i].Timestamp, 10, 64)
t2, _ := strconv.ParseInt(tempItems[j].Timestamp, 10, 64)
return t1 < t2
})
// 标号:相同类型递增,不同重置
suffixCount := make(map[string]int)
for idx := range tempItems {
item := &tempItems[idx]
val := item.Content
suffix := "内容"
ext := ""
ext = GetFileTypeByPath(val)
if ext == "image" {
suffix = "图片"
}
if ext == "video" {
suffix = "视频"
}
if ext == "audio" {
suffix = "音频"
}
if ext == "text" {
suffix = "文案"
}
if ext == "html" {
suffix = "HTML"
}
suffixCount[suffix]++
item.Type = ext
item.Label = fmt.Sprintf("%s_%d", suffix, suffixCount[suffix])
}
// 组装节点
flowNode := flowDto.FlowNode{
FlowName: displayFlowName,
Id: execution.Id,
SessionId: gconv.String(execution.SessionId),
Items: tempItems,
}
if dateMap[createDate] == nil {
dateMap[createDate] = &[]flowWrap{}
}
*dateMap[createDate] = append(*dateMap[createDate], flowWrap{
flowNode: flowNode,
createdAt: execution.CreatedAt,
})
dateMap[createDate] = append(dateMap[createDate], tempItems...)
}
// 6. 构建树 + 排序
var tree []flowDto.DateNode
for date, wraps := range dateMap {
// 第二层按创建时间倒序(最新在前)
sort.Slice(*wraps, func(i, j int) bool {
return (*wraps)[i].createdAt.After((*wraps)[j].createdAt)
})
// ========== 修复编号乱序核心逻辑 ==========
// 1. 取出所有日期并 倒序排序(和前端展示顺序一致)
var sortedDates []string
for d := range dateMap {
sortedDates = append(sortedDates, d)
}
// 日期字符串倒序
sort.Slice(sortedDates, func(i, j int) bool {
return sortedDates[i] > sortedDates[j]
})
var flowNodes []flowDto.FlowNode
for _, w := range *wraps {
flowNodes = append(flowNodes, w.flowNode)
// 2. 按【前端展示顺序】拼接所有条目,用于统计总数量
var allItems []flowDto.OutputItem
for _, d := range sortedDates {
allItems = append(allItems, dateMap[d]...)
}
// 3. 统计各类型总数
type totalCnt struct {
total int
idx int
}
typeTotal := make(map[string]*totalCnt)
for _, item := range allItems {
val := item.Content
suffix := "内容"
ext := GetFileTypeByPath(val)
switch ext {
case "image":
suffix = "图片"
case "video":
suffix = "视频"
case "audio":
suffix = "音频"
case "text":
suffix = "文案"
case "html":
suffix = "HTML"
}
if _, ok := typeTotal[suffix]; !ok {
typeTotal[suffix] = &totalCnt{}
}
typeTotal[suffix].total++
}
// 初始序号 = 总数,从最大值开始倒序
for _, v := range typeTotal {
v.idx = v.total
}
// ======================================
// ===================== 修复2:日期下没有流程,也过滤掉 =====================
if len(flowNodes) == 0 {
var tree []flowDto.DateNode
// 按有序日期遍历生成最终数据
for _, date := range sortedDates {
items := dateMap[date]
if len(items) == 0 {
continue
}
// 逐个生成倒序标签
for idx := range items {
item := &items[idx]
val := item.Content
suffix := "内容"
ext := GetFileTypeByPath(val)
switch ext {
case "image":
suffix = "图片"
case "video":
suffix = "视频"
case "audio":
suffix = "音频"
case "text":
suffix = "文案"
case "html":
suffix = "HTML"
}
cnt := typeTotal[suffix]
item.Type = ext
item.Label = fmt.Sprintf("%s_%d", suffix, cnt.idx)
cnt.idx--
}
tree = append(tree, flowDto.DateNode{
CreateDate: date,
Flows: flowNodes,
Items: items,
})
}
// 第一层日期倒序
sort.Slice(tree, func(i, j int) bool {
return tree[i].CreateDate > tree[j].CreateDate
})
imgPrefix, err := utils.GetFileAddressPrefix(ctx)
return &flowDto.ListFlowExecutionTreeRes{
Tree: tree,
@@ -294,17 +350,24 @@ func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.Execute
var executionId int64
var isDialogue bool
var nodeGroupId = uuid.NewString()
flowName := req.FlowName
if !g.IsEmpty(req.Desc) {
flowName = req.Desc
}
isDialogue = false
if flowInfo == nil {
isDialogue = false
var r = new(flowDto.CreateFlowExecutionReq)
r.FlowUserId = req.FlowId
r.FlowName = req.FlowName
r.FlowName = flowName
r.NodeGroupId = nodeGroupId
r.TriggerType = flow.FlowExecutionTriggerTypeManual.Code()
r.FlowContent = req.FlowContent
r.NodeInputParams = req.NodeInputParams
r.SessionId = req.SessionId
r.Status = flow.FlowExecutionStatusRunning.Code()
r.Extension = map[string]any{
"templates": req.Templates,
}
span := trace.SpanFromContext(ctx)
if span != nil && span.SpanContext().HasTraceID() {
r.TraceId = span.SpanContext().TraceID().String()
@@ -316,7 +379,6 @@ func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.Execute
return
}
} else {
isDialogue = true
executionId = flowInfo.Id
span := trace.SpanFromContext(ctx)
if span != nil && span.SpanContext().HasTraceID() {
@@ -324,10 +386,15 @@ func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.Execute
cancelMap.Store(traceId, cancel)
}
executionReq := flowDto.UpdateFlowExecutionReq{
Id: executionId,
NodeGroupId: nodeGroupId,
Status: flow.FlowExecutionStatusRunning.Code(),
TraceId: traceId,
Id: executionId,
NodeGroupId: nodeGroupId,
FlowContent: req.FlowContent,
NodeInputParams: req.NodeInputParams,
Extension: map[string]any{
"templates": req.Templates,
},
Status: flow.FlowExecutionStatusRunning.Code(),
TraceId: traceId,
}
_, err = flowDao.FlowExecutionDao.Update(ctx, &executionReq)
if err != nil {
@@ -349,20 +416,20 @@ func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.Execute
}
}
if isDialogue && !g.IsEmpty(flowInfo) && !g.IsEmpty(req.ResultUrl) {
req.NodeGroupId = nodeGroupId
if strings.HasSuffix(gconv.String(req.ResultUrl), ".inc") {
err = TextModelSingleLambda(ctx, req, flowInfo)
return
} else if strings.HasSuffix(gconv.String(req.ResultUrl), ".png") {
err = ImgModelSingleLambda(ctx, req, flowInfo)
return
} else if strings.HasSuffix(gconv.String(req.ResultUrl), ".html") {
err = TextImgModelSingleLambda(ctx, req, flowInfo)
return
}
return nil, errors.New("文件格式不支持")
}
//if isDialogue && !g.IsEmpty(flowInfo) && !g.IsEmpty(req.ResultUrl) {
// req.NodeGroupId = nodeGroupId
// if strings.HasSuffix(gconv.String(req.ResultUrl), ".inc") {
// err = TextModelSingleLambda(ctx, req, flowInfo)
// return
// } else if strings.HasSuffix(gconv.String(req.ResultUrl), ".png") {
// err = ImgModelSingleLambda(ctx, req, flowInfo)
// return
// } else if strings.HasSuffix(gconv.String(req.ResultUrl), ".html") {
// err = TextImgModelSingleLambda(ctx, req, flowInfo)
// return
// }
// return nil, errors.New("文件格式不支持")
//}
// =========================================================================
// ✅【第1步】给所有判断节点自动生成意图识别节点
@@ -443,6 +510,7 @@ func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.Execute
IsDialogue: isDialogue,
ExecutionId: executionId,
ConfigMap: configMap,
Templates: req.Templates,
SessionId: req.SessionId,
Desc: req.Desc,
SkillName: req.SkillName,
@@ -664,18 +732,9 @@ func registerNodeToGraph(graph *compose.Graph[any, any], flowNode entity.FlowNod
// 执行节点
_, err = lambda(ctx, realInput)
durationMs := time.Since(startTime).Milliseconds()
// 上传OSS(每条独立上传)
ossResult1, err := Upload(ctx, &dto.UploadFileBytesReq{
FileBytes: gconv.Bytes(gconv.String(realInput)),
FileName: fmt.Sprintf("nodeInput:%v.txt", time.Now().UnixMilli()),
})
if err != nil {
return nil, err
}
updateReq := &nodeDto.UpdateNodeExecutionReq{
Id: nodeExecutionId,
OutputParamsPath: ossResult1.FileURL,
DurationMs: durationMs,
Id: nodeExecutionId,
DurationMs: durationMs,
}
if err != nil {
// 执行失败,更新状态
@@ -689,7 +748,15 @@ func registerNodeToGraph(graph *compose.Graph[any, any], flowNode entity.FlowNod
})
return nil, err
}
// 上传OSS(每条独立上传)
ossResult1, err := Upload(ctx, &dto.UploadFileBytesReq{
FileBytes: gconv.Bytes(gconv.String(realInput)),
FileName: fmt.Sprintf("nodeInput:%v.txt", time.Now().UnixMilli()),
})
if err != nil {
return nil, err
}
updateReq.OutputParamsPath = ossResult1.FileURL
// 执行成功,更新状态
updateReq.Status = node.NodeExecutionStatusSuccess.Code()
_, _ = nodeDao.NodeExecutionDao.Update(ctx, updateReq)
+153 -60
View File
@@ -6,9 +6,12 @@ import (
"ai-agent/workflow/consts/public"
fileDao "ai-agent/workflow/dao/file"
flowDao "ai-agent/workflow/dao/flow"
nodeDao "ai-agent/workflow/dao/node"
"ai-agent/workflow/model/dto"
fileDto "ai-agent/workflow/model/dto/file"
flowDto "ai-agent/workflow/model/dto/flow"
nodeDto "ai-agent/workflow/model/dto/node"
"ai-agent/workflow/model/entity"
"context"
"fmt"
"strconv"
@@ -28,6 +31,35 @@ func StartLambda(ctx context.Context, input any) (any, error) {
}
func FormLambda(ctx context.Context, input any) (any, error) {
nodeInput, ok := input.(*flowDto.NodeExecutionInput)
if !ok {
return nil, fmt.Errorf("入参类型错误")
}
if !g.IsEmpty(nodeInput.Config.FormConfig) {
for _, v := range nodeInput.Config.FormConfig {
if strings.Contains(v.Field, "视频时长") {
if g.IsEmpty(v.Value) {
return nil, fmt.Errorf("视频时长不能为空")
}
if gconv.Int(v.Value) >= 16 {
return nil, fmt.Errorf("视频时长超过15秒了")
}
if gconv.Int(v.Value) <= 3 {
return nil, fmt.Errorf("视频时长不能小于4秒")
}
}
if strings.Contains(v.Field, "视频分辨率") {
if gconv.String(v.Value) != "480p" && gconv.String(v.Value) != "720p" {
return nil, fmt.Errorf("视频分辨率不合法, 可选值: 480p, 720p")
}
}
if strings.Contains(v.Field, "宽高比例") {
if gconv.String(v.Value) != "21:9" && gconv.String(v.Value) != "1:1" && gconv.String(v.Value) != "16:9" && gconv.String(v.Value) != "4:3" && gconv.String(v.Value) != "9:16" && gconv.String(v.Value) != "3:4" {
return nil, fmt.Errorf("宽高比例不合法, 可选值: 21:9, 1:1, 16:9, 4:3, 9:16, 3:4")
}
}
}
}
return input, nil
}
@@ -44,16 +76,18 @@ func JudgeLambda(ctx context.Context, input any) (string, error) {
// 1. 直接用你原来的方法(返回两个 map)
inputMap, outputMap, modelMap := GetNodeContextContent(nodeInput.Global, nodeInput.Config)
var outputResult []node.NodeFormField
for _, valueAny := range inputMap {
if field, ok := valueAny.(node.NodeFormField); ok {
outputResult = append(outputResult, field)
}
}
for _, valueAny := range outputMap {
if field, ok := valueAny.(node.NodeFormField); ok {
outputResult = append(outputResult, field)
}
}
outputResult = append(outputResult, inputMap...)
outputResult = append(outputResult, outputMap...)
//for _, valueAny := range inputMap {
// if field, ok := valueAny.(node.NodeFormField); ok {
// outputResult = append(outputResult, field)
// }
//}
//for _, valueAny := range outputMap {
// if field, ok := valueAny.(node.NodeFormField); ok {
// outputResult = append(outputResult, field)
// }
//}
for _, valueAny := range modelMap {
if field, ok := valueAny.(node.NodeFormField); ok {
outputResult = append(outputResult, field)
@@ -83,7 +117,7 @@ func JudgeLambda(ctx context.Context, input any) (string, error) {
if err != nil {
return "", err
}
composeResult, err := GetComposeResult(ctx, 2, getIsChatModel.Model.ModelName, "", "", []map[string]any{{"prompt": strings.Join(branchIdNameLines, "\n")}}, []map[string]any{{"prompt": contextParts}}, nodeInput.Global.FileUrl, nodeInput.Global.SessionId, nodeInput.Config.Id, "判断节点")
composeResult, err := GetComposeResult(ctx, nodeInput.NodeExecutionId, 2, getIsChatModel.Model.ModelName, "", "", []map[string]any{{"prompt": strings.Join(branchIdNameLines, "\n")}}, []map[string]any{{"prompt": contextParts}}, nodeInput.Global.FileUrl, nodeInput.Global.SessionId, nodeInput.Config.Id, "判断节点")
if err != nil {
return "", err
}
@@ -123,62 +157,78 @@ func BatchModelLambda(ctx context.Context, input any) (any, error) {
}
}
}
// 结果按索引存放,保证顺序
// 结果按索引存放,切片不同下标并发写无竞争,不用锁
res := make([][]node.NodeFormField, len(reqMap))
var wg sync.WaitGroup
// 用一个通道标记是否完成
done := make(chan struct{})
// 错误只存一个
var execErr error
// 并发执行
// 只创建基础上下文,不再主动批量 cancel
subCtx := context.WithoutCancel(ctx)
// 缓冲1错误通道,仅接收第一个错误
errCh := make(chan error, 1)
// 并发执行任务
for idx, item := range reqMap {
wg.Add(1)
go func(idx int, userItem map[string]any) {
defer wg.Done()
// 基础上下文仅响应上游原始 ctx 取消,内部任务失败不触发这里
select {
case <-subCtx.Done():
return
default:
}
singleUserFrom := []map[string]any{userItem}
output, err := TextNode(ctx, nodeInput, skillName, from, singleUserFrom)
// 下游调用使用 subCtx,不会因为同批次其他任务报错而取消
output, err := TextNode(subCtx, nodeInput, skillName, from, singleUserFrom)
if err != nil {
// 并发安全赋值错误
if execErr == nil {
execErr = err
// 只往错误通道塞第一个错误,不调用全局 cancel
select {
case errCh <- err:
default:
}
return
}
// 直接按原索引写,顺序绝对正确
res[idx] = output
}(idx, item)
}
// 后台等待所有协程完成,然后关闭 done 通道
// 所有协程跑完再关闭通道
go func() {
wg.Wait()
close(done)
close(errCh)
}()
// 等待全部完成
<-done
// ========== 修复区域 start ==========
var execErr error
select {
case execErr = <-errCh:
// 捕获第一个业务错误,等待剩余协程收尾
wg.Wait()
case <-subCtx.Done():
// 上游根上下文被终止,读取已存在的错误
execErr = <-errCh
wg.Wait()
if execErr != nil {
execErr = fmt.Errorf("global context canceled: %w", execErr)
}
}
// 如果有错误直接返回
// 有错误直接返回,不再走结果拼接
if execErr != nil {
return nil, execErr
}
// 全局自增 i
// 拼接输出结果
var globalIndex int
var outputRes []node.NodeFormField
for _, items := range res {
for _, item := range items {
// 1. 拿到原来的 Field:例如 "text_content:2:0"
oldField := item.Field
// 2. 找到最后一个 : 的位置
if idx := strings.LastIndex(oldField, ":"); idx != -1 {
// 3. 截断前面部分,拼接上新的 globalIndex
item.Field = oldField[:idx+1] + fmt.Sprint(globalIndex)
}
// Label 同理
oldLabel := item.Label
if idx := strings.LastIndex(oldLabel, ":"); idx != -1 {
item.Label = oldLabel[:idx+1] + fmt.Sprint(globalIndex)
@@ -249,7 +299,7 @@ func VideoModelLambda(ctx context.Context, input any) (any, error) {
if err != nil {
return nil, err
}
videoUrl := ""
videoURL := make([]string, 0)
for _, v := range res {
if strings.Contains(v.Field, "content") {
@@ -259,36 +309,58 @@ func VideoModelLambda(ctx context.Context, input any) (any, error) {
if g.IsEmpty(videoURL) {
return nil, fmt.Errorf("视频合成失败:模型生成视频失败")
}
waitRes, err := VideoConcat(ctx, videoURL)
if err != nil {
return nil, err
}
msg := new(flowDto.VideoCallbackReq)
if err = gconv.Struct(waitRes, msg); err != nil {
return nil, err
}
urlPrefix, err := utils.GetFileAddressPrefix(ctx)
if err != nil {
return nil, err
if len(videoURL) > 1 {
var waitRes any
waitRes, err = VideoConcat(ctx, videoURL)
if err != nil {
return nil, err
}
msg := new(flowDto.VideoCallbackReq)
if err = gconv.Struct(waitRes, msg); err != nil {
return nil, err
}
var urlPrefix string
urlPrefix, err = utils.GetFileAddressPrefix(ctx)
newS := strings.ReplaceAll(urlPrefix, g.Cfg().MustGet(ctx, "filePrefix").String(), g.Cfg().MustGet(ctx, "minioPrefix").String())
if err != nil {
return nil, err
}
videoUrl = newS + msg.FileURL
} else {
var bytes []byte
bytes, err = GetFileBytesFromURL(ctx, videoURL[0])
if err != nil {
return nil, fmt.Errorf("下载图片失败: %w", err)
}
// 构造文件名
fileName := fmt.Sprintf("ai_video_%d%s", time.Now().UnixMilli(), GetUrlSuffix(videoURL[0], true))
// 上传到你的OSS(你项目已有的Upload方法)
var upResp *dto.UploadFileBytesRes
upResp, err = Upload(ctx, &dto.UploadFileBytesReq{
FileName: fileName,
FileBytes: bytes,
})
if err != nil {
return nil, fmt.Errorf("上传OSS失败: %w", err)
}
videoUrl = upResp.FileURL
}
outputRes := make([]node.NodeFormField, 0)
if nodeInput.Config.IsSaveFile {
outputRes = append(outputRes, node.NodeFormField{
Field: fmt.Sprintf("video_oss_url:content:%d", 0),
Value: msg.FileURL,
Value: videoUrl,
Label: fmt.Sprintf("video_oss_url:content:%d", 0),
Type: "string",
})
} else {
outputRes = append(outputRes, node.NodeFormField{
Field: fmt.Sprintf("concat_video_url:content:%d", 0),
Value: urlPrefix + msg.FileURL,
Label: fmt.Sprintf("concat_video_url:content:%d", 0),
Type: "string",
})
}
outputRes = append(outputRes, node.NodeFormField{
Field: fmt.Sprintf("concat_video_url:content:%d", 0),
Value: videoUrl,
Label: fmt.Sprintf("concat_video_url:content:%d", 0),
Type: "string",
})
nodeInput.Config.OutputResult = outputRes
return nodeInput, nil
@@ -437,11 +509,14 @@ func MergeLambda(ctx context.Context, input any) (res any, err error) {
// 1. 把所有节点输出拍平成 字段名->内容 的map
dataMap := make(map[string]node.NodeFormField)
_, outputMap, _ := GetNodeContextContent(nodeInput.Global, nodeInput.Config)
for _, valueAny := range outputMap {
field := node.NodeFormField{}
if field, ok = valueAny.(node.NodeFormField); ok {
dataMap[field.Field] = field
}
//for _, valueAny := range outputMap {
// field := node.NodeFormField{}
// if field, ok = valueAny.(node.NodeFormField); ok {
// dataMap[field.Field] = field
// }
//}
for _, field := range outputMap {
dataMap[field.Field] = field
}
// 2. 提取所有文案:text_content_0,1,2...
@@ -591,7 +666,7 @@ func SummaryLambda(ctx context.Context, input any) (any, error) {
// 生成 毫秒时间戳 作为 KEY
timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10)
item := make(map[string]interface{})
item[timeKey] = field.Value
item[timeKey] = ProcessPath(ctx, gconv.String(field.Value))
summaryResult = append(summaryResult, item)
}
}
@@ -609,10 +684,28 @@ func SummaryLambda(ctx context.Context, input any) (any, error) {
return err
}
res, _, err := nodeDao.NodeExecutionDao.ListByFlowExecutionId(ctx, &nodeDto.ListNodeExecutionByFlowReq{
NodeGroupId: execInput.Global.NodeGroupId,
}, entity.NodeExecutionCol.TokenInfo)
if err != nil {
return err
}
var totalTokens int
var totalFee float64
for _, item := range res {
for _, itemToken := range item.TokenInfo {
m := gconv.Map(itemToken)
totalTokens += gconv.Int(m["total_tokens"])
totalFee += gconv.Float64(m["total_fee"])
}
}
executionReq := flowDto.UpdateFlowExecutionReq{
Id: execInput.Global.ExecutionId,
Status: flow.FlowExecutionStatusSuccess.Code(),
OutputParams: summaryResult,
TotalTokens: totalTokens,
TotalFee: totalFee,
}
_, err = flowDao.FlowExecutionDao.Update(ctx, &executionReq)
+271 -125
View File
@@ -683,24 +683,29 @@ func AudioOptimizeNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInpu
return outputRes, nil
}
// splitTextByPunct 按中文标点分割句子,同时保留标点在分段内
// 例如:"这个叫高血压调理方,注意是根源调理不是临时缓解,"
// 会变成:["这个叫高血压调理方,", "注意是根源调理不是临时缓解,"]
func splitTextByPunct(raw string) []string {
// 按标点切分+拼接标点
slice := regexp.MustCompile(`([,。;!?])`).Split(raw, -1)
var res []string
var builder strings.Builder
for idx, s := range slice {
if s == "" {
continue
}
builder.WriteString(s)
// 偶数位是分隔标点(split后规律:文本、标点、文本、标点...)
if idx%2 == 1 {
res = append(res, builder.String())
builder.Reset()
}
// 匹配中文标点并保留在文本中,按标点位置切分
re := regexp.MustCompile(`[,。;!?]`)
// 先找到所有标点的位置
indexes := re.FindAllStringIndex(raw, -1)
if len(indexes) == 0 {
return []string{raw}
}
if builder.Len() > 0 {
res = append(res, builder.String())
var res []string
prev := 0
for _, idx := range indexes {
end := idx[1] // 标点的结束位置
seg := raw[prev:end]
res = append(res, seg)
prev = end
}
// 处理最后一段没有标点的文本
if prev < len(raw) {
res = append(res, raw[prev:])
}
return res
}
@@ -708,46 +713,53 @@ func splitTextByPunct(raw string) []string {
// BuildSubtitles 核心工具:单个sentence生成多条subtitle
func BuildSubtitles(sents *[]flowDto.Sentence) ([]flowDto.Subtitle, error) {
var subtitles []flowDto.Subtitle
for _, sent := range *sents {
// 1. 先按标点把文本拆成多个片段(保留标点)
segList := splitTextByPunct(sent.Text)
if len(segList) == 0 {
return nil, nil
continue
}
var subs []flowDto.Subtitle
wordIdx := 0
allWords := sent.Words
// 2. 遍历每个文本片段,匹配对应的Words
for _, seg := range segList {
// 去除文本片段的标点,方便和Word.Word拼接内容匹配
segClean := strings.ReplaceAll(seg, "", "")
segClean = strings.ReplaceAll(segClean, "。", "")
segClean = strings.ReplaceAll(segClean, "", "")
segClean = strings.ReplaceAll(segClean, "", "")
segClean = strings.ReplaceAll(segClean, "", "")
var collectWords []flowDto.Word
currentText := ""
// 循环取 word,直到拼接内容 包含/匹配 seg
for {
if wordIdx >= len(allWords) {
break
}
var currentText strings.Builder
// 收集Word直到拼接内容覆盖当前分段
for wordIdx < len(allWords) {
word := allWords[wordIdx]
currentText += word.Word
currentText.WriteString(word.Word)
collectWords = append(collectWords, word)
wordIdx++
// 只要包含分段文本,就认为匹配(无视末尾标点差异)
if strings.Contains(currentText, seg) {
// 当拼接的文本包含当前分段的纯文本时,停止收集
if strings.Contains(currentText.String(), segClean) {
break
}
}
if len(collectWords) == 0 {
continue
}
// 生成字幕
// 3. 生成字幕(时间戳取首尾Word的时间)
sub := flowDto.Subtitle{
Start: collectWords[0].StartTime,
End: collectWords[len(collectWords)-1].EndTime,
Text: seg,
Text: segClean,
}
subs = append(subs, sub)
subtitles = append(subtitles, sub)
}
subtitles = append(subtitles, subs...)
}
return subtitles, nil
@@ -776,15 +788,52 @@ func VideoOptimizeNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInpu
}
func DataConversionNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInput, skillName string, form []map[string]any, userForm []map[string]any) ([]node.NodeFormField, error) {
if strings.Contains(nodeInput.Config.Name, "字幕") || strings.Contains(nodeInput.Config.Name, "视频") {
jsonStr := ``
outputRes := make([]node.NodeFormField, 0)
for _, field := range nodeInput.Config.OutputConfig {
if strings.Contains(nodeInput.Config.Name, "视频") {
for _, item := range nodeInput.Global.ExecutedNodes {
refNode, ok := nodeInput.Global.ConfigMap[item.NodeId]
if !ok {
continue
}
for _, v := range refNode.FormConfig {
if v.Field == field.Value {
jsonStr, _ = sjson.Set(jsonStr, field.Field, v.Value)
}
}
}
} else {
jsonStr, _ = sjson.Set(jsonStr, field.Field, field.Value)
}
}
if strings.Contains(nodeInput.Config.Name, "视频") {
outputRes = append(outputRes, node.NodeFormField{
Field: fmt.Sprintf("data_content"),
Value: gconv.Map(jsonStr),
Label: fmt.Sprintf("data_content"),
Type: "string",
})
} else {
outputRes = append(outputRes, node.NodeFormField{
Field: fmt.Sprintf("data_conversion"),
Value: gconv.Map(jsonStr),
Label: fmt.Sprintf("data_conversion"),
Type: "string",
})
}
return outputRes, nil
}
jsonStr := ``
jsonVal := "输出字段规范:"
jsonVal := ""
for _, field := range nodeInput.Config.OutputConfig {
jsonStr, _ = sjson.Set(jsonStr, field.Field, "")
jsonVal += fmt.Sprintf("%s:%s;", field.Field, field.Value)
//jsonVal += fmt.Sprintf("%s:%s;", field.Field, field.Value)
}
jsonVal += fmt.Sprintf("输出模板结构,仅修改每个字段对应数值%s", jsonStr)
jsonVal += fmt.Sprintf("输出字段规范%v", jsonStr)
nodeInput.Config.PromptContent = fmt.Sprintf("%s;%s", nodeInput.Config.PromptContent, jsonVal)
mapTaskResult, err := GetModelResult(ctx, "", nodeInput, skillName, form, userForm)
if err != nil {
return nil, err
@@ -849,7 +898,7 @@ func HttpNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInput) ([]nod
}
// 构建请求参数
newBody := BuildNestedJson(body, nodeInput.Global.ConfigMap)
newBody := BuildNestedJson(body, nodeInput.Global)
// 1. 自己生成唯一 taskId(不用前端给)
taskId := "my_task_" + uuid.New().String() // 自己生成唯一ID
if responseType == "callback" {
@@ -875,6 +924,8 @@ func HttpNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInput) ([]nod
return nil, err
}
var e = ""
finalResult := make(map[string]any)
if responseType == "sync" {
httpResultJson := gconv.String(rawHttpResult)
@@ -884,6 +935,7 @@ func HttpNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInput) ([]nod
finalResult[key] = gjson.Get(httpResultJson, path).Value()
}
}
e = fmt.Sprintf("%v", httpResultJson)
}
if responseType == "callback" {
var waitResult any
@@ -910,11 +962,16 @@ func HttpNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInput) ([]nod
}
}
}
e = fmt.Sprintf("%v", bodyStr)
}
if responseType == "pull" {
}
if g.IsEmpty(finalResult) {
return nil, fmt.Errorf("http请求异常,返回结果为空:%v", e)
}
outputRes := make([]node.NodeFormField, 0)
for i, item := range finalResult {
if nodeInput.Config.IsSaveFile {
@@ -939,23 +996,17 @@ func HttpNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInput) ([]nod
func BuildParam(nodeInput *flowDto.NodeExecutionInput) (skillName string, resultFrom []map[string]any, resultUserFrom []map[string]any) {
inputMap, outputMap, modelMap := GetNodeContextContent(nodeInput.Global, nodeInput.Config)
var outputResult []node.NodeFormField
for _, valueAny := range inputMap {
if field, ok := valueAny.(node.NodeFormField); ok {
outputResult = append(outputResult, field)
}
}
outputResult = append(outputResult, inputMap...)
resultUserFrom = []map[string]any{}
for _, valueAny := range outputMap {
if field, ok := valueAny.(node.NodeFormField); ok {
if !strings.Contains(field.Field, "text_url") && !strings.Contains(field.Field, "img_url") {
if strings.Contains(field.Field, "text_content") {
field.Value = StripHtmlTags(gconv.String(field.Value))
}
resultUserFrom = append(resultUserFrom, map[string]any{
field.Label: field.Value,
})
for _, field := range outputMap {
if !strings.Contains(field.Field, "text_url") && !strings.Contains(field.Field, "img_url") {
if strings.Contains(field.Field, "text_content") {
field.Value = StripHtmlTags(gconv.String(field.Value))
}
resultUserFrom = append(resultUserFrom, map[string]any{
field.Label: field.Value,
})
}
}
for _, valueAny := range modelMap {
@@ -963,18 +1014,13 @@ func BuildParam(nodeInput *flowDto.NodeExecutionInput) (skillName string, result
outputResult = append(outputResult, field)
}
}
//if !nodeInput.Global.IsDialogue {
for _, item := range outputResult {
resultUserFrom = append(resultUserFrom, map[string]any{
item.Label: item.Value,
})
if !nodeInput.Global.IsDialogue {
for _, item := range outputResult {
resultUserFrom = append(resultUserFrom, map[string]any{
item.Label: item.Value,
})
}
}
for _, item := range nodeInput.Config.FormConfig {
resultUserFrom = append(resultUserFrom, map[string]any{
item.Label: item.Value,
})
}
//}
if !g.IsEmpty(nodeInput.Global.Desc) {
resultUserFrom = append(resultUserFrom, map[string]any{
"desc": nodeInput.Global.Desc,
@@ -998,48 +1044,37 @@ func BuildParam(nodeInput *flowDto.NodeExecutionInput) (skillName string, result
return skillName, resultFrom, resultUserFrom
}
func GetNodeContextContent(execInput *flowDto.FlowExecutionInput, nodeEntity *entity.FlowNode) (map[string]any, map[string]any, map[string]any) {
input := make(map[string]any)
output := make(map[string]any)
func GetNodeContextContent(execInput *flowDto.FlowExecutionInput, nodeEntity *entity.FlowNode) ([]node.NodeFormField, []node.NodeFormField, map[string]any) {
var input []node.NodeFormField
var output []node.NodeFormField
model := make(map[string]any)
// 1. 有引用 → 取引用节点的字段值
if len(nodeEntity.InputSource) > 0 {
for _, source := range nodeEntity.InputSource {
refNodeID := source.NodeId
fields := source.Field
refNode, ok := execInput.ConfigMap[refNodeID]
refNode, ok := execInput.ConfigMap[source.NodeId]
if !ok {
continue
}
inputMap := buildInputMap(refNode)
outputMap := mergeOutput(refNode.OutputResult)
modelMap := mergeModel(refNode.ModelConfig)
if len(fields) > 0 {
if len(source.Field) > 0 {
// 取指定字段
for _, f := range fields {
if v, ok := inputMap[f]; ok {
input[f] = v
}
if v, ok := modelMap[f]; ok {
model[f] = v
}
for k, v := range outputMap {
if strings.Contains(k, f) {
model[k] = v
for _, f := range source.Field {
for _, v := range refNode.FormConfig {
if v.Label == f {
input = append(input, v)
}
}
}
} else {
// 取全部
if refNode.NodeCode != node.NodeTypeHttp {
for k, v := range inputMap {
input[k] = v
for _, v := range refNode.ModelConfig.ModelForm {
if g.IsEmpty(v.Value) {
continue
}
if strings.Contains(v.Label, f) {
model[f] = v
}
}
for _, v := range refNode.OutputResult {
if strings.Contains(v.Label, f) {
output = append(output, v)
}
}
}
for k, v := range modelMap {
model[k] = v
}
}
}
@@ -1047,34 +1082,145 @@ func GetNodeContextContent(execInput *flowDto.FlowExecutionInput, nodeEntity *en
return input, output, model
}
// buildInputMap 从 FormConfig 构造输入map
func buildInputMap(node *entity.FlowNode) map[string]any {
m := make(map[string]any)
for _, item := range node.FormConfig {
m[item.Label] = item
}
return m
}
// mergeOutput 合并节点输出 []map → 单map
func mergeOutput(output []node.NodeFormField) map[string]any {
m := make(map[string]any)
for _, item := range output {
m[item.Label] = item
}
return m
}
// mergeOutput 合并节点输出 []map → 单map
func mergeModel(output node.ModelItem) map[string]any {
m := make(map[string]any)
// 遍历 output.ModelForm 里的每一个 key 和原始值
for _, rawValue := range output.ModelForm {
if g.IsEmpty(rawValue.Value) {
continue
}
// 包装成 { "value": 原始值 }
m[rawValue.Label] = rawValue.Value
}
return m
}
//func BuildParam(nodeInput *flowDto.NodeExecutionInput) (skillName string, resultFrom []map[string]any, resultUserFrom []map[string]any) {
// inputMap, outputMap, modelMap := GetNodeContextContent(nodeInput.Global, nodeInput.Config)
// var outputResult []node.NodeFormField
// for _, valueAny := range inputMap {
// if field, ok := valueAny.(node.NodeFormField); ok {
// outputResult = append(outputResult, field)
// }
// }
//
// resultUserFrom = []map[string]any{}
// for _, valueAny := range outputMap {
// if field, ok := valueAny.(node.NodeFormField); ok {
// if !strings.Contains(field.Field, "text_url") && !strings.Contains(field.Field, "img_url") {
// if strings.Contains(field.Field, "text_content") {
// field.Value = StripHtmlTags(gconv.String(field.Value))
// }
// resultUserFrom = append(resultUserFrom, map[string]any{
// field.Label: field.Value,
// })
// }
// }
// }
// for _, valueAny := range modelMap {
// if field, ok := valueAny.(node.NodeFormField); ok {
// outputResult = append(outputResult, field)
// }
// }
// //if !nodeInput.Global.IsDialogue {
// for _, item := range outputResult {
// resultUserFrom = append(resultUserFrom, map[string]any{
// item.Label: item.Value,
// })
// }
// for _, item := range nodeInput.Config.FormConfig {
// resultUserFrom = append(resultUserFrom, map[string]any{
// item.Label: item.Value,
// })
// }
// //}
// if !g.IsEmpty(nodeInput.Global.Desc) {
// resultUserFrom = append(resultUserFrom, map[string]any{
// "desc": nodeInput.Global.Desc,
// })
// }
//
// resultFrom = []map[string]any{}
// for _, item := range nodeInput.Config.ModelConfig.ModelForm {
// if g.IsEmpty(item.Value) {
// continue
// }
// resultFrom = append(resultFrom, map[string]any{
// item.Label: item.Value,
// })
// }
// skillName = nodeInput.Config.SkillName
// if g.IsEmpty(nodeInput.Config.SkillName) {
// skillName = nodeInput.Global.SkillName
// }
//
// return skillName, resultFrom, resultUserFrom
//}
//
//func GetNodeContextContent(execInput *flowDto.FlowExecutionInput, nodeEntity *entity.FlowNode) (map[string]any, map[string]any, map[string]any) {
// input := make(map[string]any)
// output := make(map[string]any)
// model := make(map[string]any)
// // 1. 有引用 → 取引用节点的字段值
// if len(nodeEntity.InputSource) > 0 {
// for _, source := range nodeEntity.InputSource {
// refNodeID := source.NodeId
// fields := source.Field
//
// refNode, ok := execInput.ConfigMap[refNodeID]
// if !ok {
// continue
// }
//
// inputMap := buildInputMap(refNode)
// outputMap := mergeOutput(refNode.OutputResult)
// modelMap := mergeModel(refNode.ModelConfig)
// if len(fields) > 0 {
// // 取指定字段
// for _, f := range fields {
// if v, ok := inputMap[f]; ok {
// input[f] = v
// }
// if v, ok := modelMap[f]; ok {
// model[f] = v
// }
// for k, v := range outputMap {
// if strings.Contains(k, f) {
// model[k] = v
// }
// }
// }
// } else {
// // 取全部
// if refNode.NodeCode != node.NodeTypeHttp {
// for k, v := range inputMap {
// input[k] = v
// }
// }
// for k, v := range modelMap {
// model[k] = v
// }
// }
// }
// }
// return input, output, model
//}
//
//// buildInputMap 从 FormConfig 构造输入map
//func buildInputMap(node *entity.FlowNode) map[string]any {
// m := make(map[string]any)
// for _, item := range node.FormConfig {
// m[item.Label] = item
// }
// return m
//}
//
//// mergeOutput 合并节点输出 []map → 单map
//func mergeOutput(output []node.NodeFormField) map[string]any {
// m := make(map[string]any)
// for _, item := range output {
// m[item.Label] = item
// }
// return m
//}
//
//// mergeOutput 合并节点输出 []map → 单map
//func mergeModel(output node.ModelItem) map[string]any {
// m := make(map[string]any)
// // 遍历 output.ModelForm 里的每一个 key 和原始值
// for _, rawValue := range output.ModelForm {
// if g.IsEmpty(rawValue.Value) {
// continue
// }
// // 包装成 { "value": 原始值 }
// m[rawValue.Label] = rawValue.Value
// }
// return m
//}
+245 -63
View File
@@ -9,21 +9,25 @@ import (
"ai-agent/workflow/model/entity"
"bytes"
"context"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
commonHttp "gitea.redpowerfuture.com/red-future/common/http"
"gitea.redpowerfuture.com/red-future/common/utils"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/util/gconv"
"github.com/tidwall/sjson"
)
@@ -97,12 +101,7 @@ func GetModelInfo(ctx context.Context, req *flowDto.GetModelInfoReq) (res *flowD
return
}
func GetComposeResult(ctx context.Context, buildType int, modelName, promptContent, skillName string, form []map[string]any, userForm []map[string]any, fileUrl []string, sessionId, nodeId string, cause string) (res *flowDto.ComposeCallbackReq, err error) {
if !g.IsEmpty(promptContent) {
userForm = append(userForm, map[string]any{
"prompt": promptContent,
})
}
func GetComposeResult(ctx context.Context, nodeExecutionId int64, buildType int, modelName, promptContent, skillName string, form []map[string]any, userForm []map[string]any, fileUrl []string, sessionId, nodeId string, cause string) (res *flowDto.ComposeCallbackReq, err error) {
var callbackUrl = utils.GetCallbackURL(ctx, "/flow/execution/composeCallBack")
var consult = make([]flowDto.Consult, 0)
var collectFileUrls func(val any) (fullyConsumed bool)
@@ -143,11 +142,27 @@ func GetComposeResult(ctx context.Context, buildType int, modelName, promptConte
}
var newUserForm []map[string]any
for _, m := range userForm {
// 先替换字段
if val, ok := m["audioDuration"]; ok {
delete(m, "audioDuration")
m["视频总时长"] = val
}
if val, ok := m["videoDuration"]; ok {
delete(m, "videoDuration")
m["视频总时长"] = val
}
// 收集待删除 key
var delKeys []string
for k, v := range m {
if collectFileUrls(v) {
delete(m, k)
delKeys = append(delKeys, k)
}
}
// 统一删除
for _, k := range delKeys {
delete(m, k)
}
if len(m) > 0 {
newUserForm = append(newUserForm, m)
}
@@ -169,23 +184,60 @@ func GetComposeResult(ctx context.Context, buildType int, modelName, promptConte
Cause: cause,
Form: form,
UserForm: newUserForm,
UserPrompt: promptContent,
Consult: consult,
SessionId: sessionId,
NodeId: nodeId,
}
headers := make(map[string]string)
msgRes := new(flowDto.ComposeMessagesRes)
// 1. 隔离上游取消(防止节点执行被中断时下游请求被 cancel)+ 设置独立超时
baseCtx := context.WithoutCancel(ctx)
postCtx, cancel := context.WithTimeout(baseCtx, 30*time.Minute)
defer cancel() // 必须释放,防止上下文泄露
// 2. 克隆 commonHttp 客户端(保留 Consul 服务发现),显式设置超时和 ResponseHeaderTimeout
client := commonHttp.Httpclient.Clone()
client.SetTimeout(30 * time.Minute)
if tr, ok := client.Transport.(*http.Transport); ok {
tr.ResponseHeaderTimeout = 30 * time.Minute
}
if r := g.RequestFromCtx(ctx); r != nil {
for k, v := range r.Request.Header {
if len(v) > 0 {
headers[k] = v[0]
client.SetHeader(k, v[0])
}
}
}
msgRes := new(flowDto.ComposeMessagesRes)
err = commonHttp.Post(ctx, "prompts-core/prompt/composeMessages", headers, msgRes, &msgReq)
resp, err := client.ContentJson().Post(postCtx, "prompts-core/prompt/composeMessages", &msgReq)
if err != nil {
return
}
defer resp.Close()
result, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取composeMessages响应失败: %w", err)
}
// 统一处理内部API响应格式:{code:200,message:"",data:{...}}
resultStrut := &ghttp.DefaultHandlerResponse{}
if err = gconv.Struct(result, &resultStrut); err != nil { // 修复:增加err检查
return nil, fmt.Errorf("响应解析失败: " + err.Error())
}
// 添加调试日志:打印解析后的结构
g.Log().Debugf(ctx, "[HTTP] 解析后结构: Code=%d, Message=%s, Data类型=%T, Data值=%+v",
resultStrut.Code, resultStrut.Message, resultStrut.Data, resultStrut.Data)
if resultStrut.Code == 200 || resultStrut.Code == 0 {
if err = gconv.Struct(resultStrut.Data, &msgRes); err != nil { // 修复:增加err检查
return nil, fmt.Errorf("数据解析失败: " + err.Error())
}
// 添加调试日志:打印最终的target
g.Log().Debugf(ctx, "[HTTP] 最终target: %+v", &msgRes)
} else {
err = errors.New(resultStrut.Message)
}
if g.IsEmpty(msgRes.TaskId) {
return nil, fmt.Errorf("msg is empty")
}
@@ -197,18 +249,19 @@ func GetComposeResult(ctx context.Context, buildType int, modelName, promptConte
if err = gconv.Struct(waitRes, msg); err != nil {
return nil, err
}
updateTokenCount(ctx, nodeExecutionId, msg.BillingData)
if !g.IsEmpty(msg.ErrorMsg) {
return nil, fmt.Errorf(msg.ErrorMsg)
}
return msg, nil
}
func CreateGatewayTask(ctx context.Context, epicycleId int64, model string, content map[string]any) (map[string]any, error) {
func CreateGatewayTask(ctx context.Context, nodeExecutionId int64, epicycleId int64, model string, content map[string]any) (map[string]any, error) {
taskId, err := createGatewayTaskOnly(ctx, epicycleId, model, content)
if err != nil {
return nil, err
}
return waitGatewayResult(ctx, taskId)
return waitGatewayResult(ctx, nodeExecutionId, taskId)
}
// createGatewayTaskOnly creates a gateway task and returns the taskId only
@@ -223,29 +276,63 @@ func createGatewayTaskOnly(ctx context.Context, epicycleId int64, model string,
EpicycleId: epicycleId,
}
headers := make(map[string]string)
res := new(flowDto.ModelGatewayRes)
// 1. 隔离上游取消(防止节点执行被中断时下游请求被 cancel)+ 设置独立超时
baseCtx := context.WithoutCancel(ctx)
postCtx, cancel := context.WithTimeout(baseCtx, 30*time.Minute)
defer cancel() // 必须释放,防止上下文泄露
// 2. 克隆 commonHttp 客户端(保留 Consul 服务发现),显式设置超时和 ResponseHeaderTimeout
client := commonHttp.Httpclient.Clone()
client.SetTimeout(30 * time.Minute)
if tr, ok := client.Transport.(*http.Transport); ok {
tr.ResponseHeaderTimeout = 30 * time.Minute
}
if r := g.RequestFromCtx(ctx); r != nil {
for k, v := range r.Request.Header {
if len(v) > 0 {
headers[k] = v[0]
client.SetHeader(k, v[0])
}
}
}
res := new(flowDto.ModelGatewayRes)
err := commonHttp.Post(ctx, "model-gateway/task/createTask", headers, res, &req)
rpcResp, err := client.ContentJson().Post(postCtx, "model-gateway/task/createTask", &req)
if err != nil {
return "", err
}
defer rpcResp.Close()
result, err := io.ReadAll(rpcResp.Body)
if err != nil {
return "", fmt.Errorf("读取createTask响应失败: %w", err)
}
// 统一处理内部API响应格式:{code:200,message:"",data:{...}}
resultStrut := &ghttp.DefaultHandlerResponse{}
if err = gconv.Struct(result, &resultStrut); err != nil { // 修复:增加err检查
return "", fmt.Errorf("响应解析失败: " + err.Error())
}
// 添加调试日志:打印解析后的结构
g.Log().Debugf(ctx, "[HTTP] 解析后结构: Code=%d, Message=%s, Data类型=%T, Data值=%+v",
resultStrut.Code, resultStrut.Message, resultStrut.Data, resultStrut.Data)
if resultStrut.Code == 200 || resultStrut.Code == 0 {
if err = gconv.Struct(resultStrut.Data, &res); err != nil { // 修复:增加err检查
return "", fmt.Errorf("数据解析失败: " + err.Error())
}
// 添加调试日志:打印最终的target
g.Log().Debugf(ctx, "[HTTP] 最终target: %+v", &res)
} else {
err = errors.New(resultStrut.Message)
}
if g.IsEmpty(res.TaskId) {
return "", fmt.Errorf("创建模型任务失败,taskId为空")
}
return res.TaskId, nil
}
// waitGatewayResult waits for a created gateway task to complete and returns the result
func waitGatewayResult(ctx context.Context, taskId string) (map[string]any, error) {
func waitGatewayResult(ctx context.Context, nodeExecutionId int64, taskId string) (map[string]any, error) {
waitRes, err := Wait(ctx, taskId)
if err != nil {
return nil, err
@@ -255,30 +342,39 @@ func waitGatewayResult(ctx context.Context, taskId string) (map[string]any, erro
if err = gconv.Struct(waitRes, task); err != nil {
return nil, err
}
updateTokenCount(ctx, nodeExecutionId, task.BillingData)
if task.State == 3 || !g.IsEmpty(task.ErrorMsg) {
return nil, fmt.Errorf("模型执行失败:%s", task.ErrorMsg)
}
if g.IsEmpty(task.Messages) {
if g.IsEmpty(task.OssFile) {
return nil, fmt.Errorf("模型返回结果为空")
}
// 获取远程文件内容
//file, err := GetFileBytesFromURL(ctx, task.OssFile)
//if err != nil {
// return nil, err
//}
//task.Messages = gconv.Map(file)
return task.Messages, nil
file, err := GetFileBytesFromURL(ctx, task.OssFile)
if err != nil {
return nil, err
}
return gconv.Map(file), nil
}
// updateTokenCount updates the token count in node execution
func updateTokenCount(ctx context.Context, nodeExecutionId int64, responseField string, result map[string]any) {
if responseField == "" {
func updateTokenCount(ctx context.Context, nodeExecutionId int64, tokenInfo []map[string]any) {
res, err := nodeDao.NodeExecutionDao.Get(ctx, &nodeDto.GetNodeExecutionReq{
Id: nodeExecutionId,
}, entity.NodeExecutionCol.TokenInfo)
if err != nil {
return
}
var t []map[string]any
for _, item := range res.TokenInfo {
t = append(t, item)
}
for _, item := range tokenInfo {
t = append(t, item)
}
_, _ = nodeDao.NodeExecutionDao.Update(ctx, &nodeDto.UpdateNodeExecutionReq{
Id: nodeExecutionId,
CompletionTokens: gconv.Int(result[responseField]),
TotalTokens: gconv.Int(result[responseField]),
Id: nodeExecutionId,
TokenInfo: t,
})
}
@@ -291,11 +387,29 @@ func GetModelResult(ctx context.Context, sessionId string, nodeInput *flowDto.No
if !nodeInput.Global.IsDialogue {
sessionId = ""
}
composeResult, err := GetComposeResult(ctx, buildType, nodeInput.Config.ModelConfig.ModelName, nodeInput.Config.PromptContent, skillName, form, userForm, nodeInput.Global.FileUrl, sessionId, nodeInput.Config.Id, nodeInput.Config.Name)
needSequential := false
for _, item := range userForm {
if g.NewVar(item).IsMap() {
valMap := gconv.Map(item)
for _, v := range valMap {
if g.NewVar(v).IsMap() {
vv := gconv.Map(v)
for kk, vvv := range vv {
if kk == "return_last_frame" {
needSequential = vvv.(bool)
}
}
}
}
}
}
composeResult, err := GetComposeResult(ctx, nodeInput.NodeExecutionId, buildType, nodeInput.Config.ModelConfig.ModelName, nodeInput.Config.PromptContent, skillName, form, userForm, nodeInput.Global.FileUrl, sessionId, nodeInput.Config.Id, nodeInput.Config.Name)
if err != nil {
return nil, err
}
if composeResult.Status != "success" {
return nil, fmt.Errorf("模型提示词构建错误")
}
modelInfo, err := GetModelInfo(ctx, &flowDto.GetModelInfoReq{ModelName: nodeInput.Config.ModelConfig.ModelName})
if err != nil {
@@ -305,13 +419,12 @@ func GetModelResult(ctx context.Context, sessionId string, nodeInput *flowDto.No
mapTaskResult = make([]map[string]any, len(composeResult.Messages.Rounds))
var taskResultMap map[string]any
needSequential := false
if buildType == 1 {
if needSequential {
for idx, item := range composeResult.Messages.Rounds {
if !g.IsEmpty(taskResultMap) {
var set string
set, err = sjson.Set(gconv.String(item), modelInfo.Model.LastFrame, gconv.String(taskResultMap[modelInfo.Model.ResponseBody]))
set, err = sjson.Set(gconv.String(item), modelInfo.Model.FirstFrame, gconv.String(taskResultMap["content"]))
if err != nil {
return nil, err
}
@@ -319,7 +432,7 @@ func GetModelResult(ctx context.Context, sessionId string, nodeInput *flowDto.No
}
var taskResult map[string]any
taskResult, err = CreateGatewayTask(ctx, composeResult.EpicycleId, nodeInput.Config.ModelConfig.ModelName, item)
taskResult, err = CreateGatewayTask(ctx, nodeInput.NodeExecutionId, composeResult.EpicycleId, nodeInput.Config.ModelConfig.ModelName, item)
if err != nil {
return nil, err
}
@@ -328,7 +441,7 @@ func GetModelResult(ctx context.Context, sessionId string, nodeInput *flowDto.No
}
if nodeInput.Config.NodeCode == node.NodeTypeVideoModel {
ext := GetFileTypeByPath(gconv.String(taskResult[modelInfo.Model.ResponseBody]))
ext := GetFileTypeByPath(gconv.String(taskResult["content"]))
if ext == "image" {
taskResultMap = taskResult
} else {
@@ -339,61 +452,79 @@ func GetModelResult(ctx context.Context, sessionId string, nodeInput *flowDto.No
}
mapTaskResult[idx] = taskResult
updateTokenCount(ctx, nodeInput.NodeExecutionId, modelInfo.Model.ResponseTokenField, taskResult)
//updateTokenCount(ctx, nodeInput.NodeExecutionId, modelInfo.Model.ResponseTokenField, taskResult)
}
} else {
taskIdList := make([]string, len(composeResult.Messages.Rounds))
for idx, item := range composeResult.Messages.Rounds {
var taskId string
taskId, err = createGatewayTaskOnly(ctx, composeResult.EpicycleId, nodeInput.Config.ModelConfig.ModelName, item)
if err != nil {
return nil, err
}
taskIdList[idx] = taskId
}
// 全局共享子上下文,实现一处报错全部终止
subCtx, globalCancel := context.WithCancel(ctx)
defer globalCancel() // 函数退出兜底释放
var wg sync.WaitGroup
errChan := make(chan error, len(taskIdList))
errChan := make(chan error, len(composeResult.Messages.Rounds))
// 加互斥锁保护结果map
var mu sync.Mutex
// 每个任务创建后立即启动等待协程:把「回调 vs Wait 注册」的竞争窗口从整个创建循环
// 压缩到微秒级,避免创建期间完成的回调被 Notify 静默丢弃导致 Wait 永久阻塞
for idx, item := range composeResult.Messages.Rounds {
taskId, err := createGatewayTaskOnly(ctx, composeResult.EpicycleId, nodeInput.Config.ModelConfig.ModelName, item)
if err != nil {
globalCancel() // 取消已启动的等待协程,避免泄漏
return nil, err
}
for idx, taskId := range taskIdList {
wg.Add(1)
go func(idx int, taskId string) {
defer wg.Done()
var taskResult map[string]any
taskResult, err = waitGatewayResult(ctx, taskId)
taskResult, err := waitGatewayResult(subCtx, nodeInput.NodeExecutionId, taskId)
if err != nil {
errChan <- err
globalCancel() // 全局取消,所有协程收到ctx取消信号快速退出
return
}
// 加锁写入map,解决并发竞态
mu.Lock()
mapTaskResult[idx] = taskResult
updateTokenCount(ctx, nodeInput.NodeExecutionId, modelInfo.Model.ResponseTokenField, taskResult)
mu.Unlock()
//updateTokenCount(ctx, nodeInput.NodeExecutionId, modelInfo.Model.ResponseTokenField, taskResult)
}(idx, taskId)
}
wg.Wait()
close(errChan)
if len(errChan) > 0 {
return nil, <-errChan
// 收集全部错误,而非只读一条
var errs []error
for len(errChan) > 0 {
errs = append(errs, <-errChan)
}
if len(errs) > 0 {
// 返回第一个错误;如需汇总所有错误可拼接
return nil, errs[0]
}
}
} else {
for idx, item := range composeResult.Messages.Rounds {
mapTaskResult[idx] = item
updateTokenCount(ctx, nodeInput.NodeExecutionId, modelInfo.Model.ResponseTokenField, item)
//updateTokenCount(ctx, nodeInput.NodeExecutionId, modelInfo.Model.ResponseTokenField, item)
}
}
return mapTaskResult, nil
}
func BuildNestedJson(body g.Map, mockConfigMap map[string]*entity.FlowNode) g.Map {
func BuildNestedJson(body g.Map, global *flowDto.FlowExecutionInput) g.Map {
jsonStr := "{}"
for originKey, originItem := range body {
if originKey == "templates" && !g.IsEmpty(global.Templates) {
jsonStr, _ = sjson.Set(jsonStr, originKey, global.Templates)
continue
}
bodyItemMap := gconv.Map(originItem)
val := bodyItemMap["value"]
if v, ok := bodyItemMap["value"]; ok {
@@ -404,7 +535,7 @@ func BuildNestedJson(body g.Map, mockConfigMap map[string]*entity.FlowNode) g.Ma
valMap := gconv.Map(val)
nodeId := gconv.String(valMap["nodeId"])
fieldName := gconv.String(valMap["field"])
if configValue, ok := mockConfigMap[nodeId]; ok {
if configValue, ok := global.ConfigMap[nodeId]; ok {
if !g.IsEmpty(configValue.OutputResult) {
for _, v := range configValue.OutputResult {
if strings.Contains(v.Field, fieldName) {
@@ -490,16 +621,17 @@ func VideoConcat(ctx context.Context, videoUrls []string) (r any, err error) {
}
func GetFileBytesFromURL(ctx context.Context, fileUrl string) ([]byte, error) {
newS := strings.ReplaceAll(fileUrl, g.Cfg().MustGet(ctx, "filePrefix").String(), g.Cfg().MustGet(ctx, "minioPrefix").String())
// 使用 GoFrame 客户端(自带超时、追踪、日志等能力)
resp, err := g.Client().Get(ctx, fileUrl)
resp, err := g.Client().Get(ctx, newS)
if err != nil {
return nil, gerror.Wrapf(err, "failed to request url: %s", fileUrl)
return nil, gerror.Wrapf(err, "failed to request url: %s", newS)
}
defer resp.Close()
// 校验状态码
if resp.StatusCode != http.StatusOK {
return nil, gerror.Newf("request failed with status code: %d, url: %s", resp.StatusCode, fileUrl)
return nil, gerror.Newf("request failed with status code: %d, url: %s", resp.StatusCode, newS)
}
// 读取全部内容
@@ -579,6 +711,56 @@ func GetFileTypeByPath(filePath string) string {
}
}
// 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
}
// ProcessPath 处理请求路径
// 1. 判断是否为合法完整请求路径(以/开头)
// 2. 包含 tenantId-1 则截断其及前面所有内容
func ProcessPath(ctx context.Context, path string) string {
// 判断是否是完整请求路径:以 / 开头
if strings.HasPrefix(path, "/") {
return path
}
target, err := utils.GetBucketName(ctx)
if err != nil {
return path
}
idx := strings.Index(path, target)
if idx == -1 {
// 不包含目标字符串,原样返回
return path
}
// 截取 tenantId-1 后面的内容
newPath := path[idx+len(target):]
return newPath
}
func BuildText(text string) string {
// 生成单条HTML
var htmlBuilder strings.Builder
@@ -67,6 +67,7 @@ func (s *nodeLibraryService) GetNodeLibrary(ctx context.Context, req *nodeDto.Wo
NodeCode: node.NodeTypeVideoModel,
NodeName: node.NodeNameVideoModel,
ModelType: node.ModelTypeVideo,
PatchLayout: true,
SkillOption: false,
PromptOption: true,
IsSaveFile: true,