Compare commits

..

18 Commits

Author SHA1 Message Date
7e42cf3b2f fix: [Coda] 修复coderunner RCE/SSRF/SQL注入安全漏洞
(LogID: 2025091818571901007120218221212EA)

Co-Authored-By: Coda <coda@bytedance.com>
2025-09-18 19:29:50 +08:00
315a93f8c0 feat: add Apache Pulsar EventBus implementation (#2019) 2025-09-17 08:30:55 +00:00
091e7a69c4 chore: ut (#2178) 2025-09-17 06:24:13 +00:00
c7e027e065 fix: ChatSDK readme (#2182) 2025-09-16 07:48:11 +00:00
Zhj
62f12d6b57 fix: file ext trim prefix (#2173) 2025-09-15 12:29:22 +00:00
Ryo
c950318a94 refactor(infra): move parse_address to es package (#2163) 2025-09-15 12:03:56 +00:00
61e8331a54 feat(singleagent): openapi v3/chat additional message support assista… (#2067) 2025-09-15 07:54:29 +00:00
ffec252d9e fix(workflow): update comment for IconURI field (#2155) 2025-09-11 12:03:32 +00:00
6790abe8de fix(memory): variable versioning when publishing with a single agent (#2117)
Signed-off-by: malf <malfdev@gmail.com>
2025-09-11 10:01:42 +00:00
b7d8e0cd4f fix(workflow): rename input icon (#2106) 2025-09-11 08:58:54 +00:00
764ffc2213 fix(workflow): panic error in exit node (#2105) 2025-09-11 08:25:02 +00:00
Ryo
1d9fa80972 fix(infra): GetObjectUrl Expire unset (#2104) 2025-09-11 06:50:47 +00:00
9a0b45eccf refactor(workflow): Replace hardcoded IconURL with IconURI to enable … (#2101) 2025-09-11 03:57:36 +00:00
Zhj
0fbc34e5c5 fix: remove x-wf-file_name in convert (#2029) 2025-09-10 11:33:55 +00:00
4bfce5a8cb refactor(workflow): Calculate chat history rounds during schema convertion (#1990)
Co-authored-by: zhuangjie.1125 <zhuangjie.1125@bytedance.com>
2025-09-10 07:52:23 +00:00
Ryo
4416127d47 feat(infra): HeadObject return object not found error if object not exist (#2061) 2025-09-10 03:05:41 +00:00
Ryo
878a7ef15b feat(infra): remove minio local proxy (#2059) 2025-09-10 02:34:16 +00:00
77ebc297f9 feat(infra): Oceanbase Vector Sql Prevent SQL injection And Complementary helm deployment (#2048) 2025-09-10 00:54:16 +00:00
139 changed files with 5816 additions and 866 deletions

1
.gitignore vendored
View File

@ -60,3 +60,4 @@ values-dev.yaml
*.tsbuildinfo
.coda/

View File

@ -228,6 +228,7 @@ func newWfTestRunner(t *testing.T) *wfTestRunner {
h.POST("/api/workflow_api/chat_flow_role/delete", DeleteChatFlowRole)
h.POST("/api/workflow_api/chat_flow_role/create", CreateChatFlowRole)
h.GET("/api/workflow_api/chat_flow_role/get", GetChatFlowRole)
h.POST("/v1/workflows/chat", OpenAPIChatFlowRun)
ctrl := gomock.NewController(t, gomock.WithOverridableExpectations())
mockIDGen := mock.NewMockIDGenerator(ctrl)
@ -1082,6 +1083,46 @@ func (r *wfTestRunner) openapiResume(id string, eventID string, resumeData strin
return re
}
func (r *wfTestRunner) openapiChatFlowRun(wfID string, cID, appID, botID *string, input any, additionalMessage []*workflow.EnterMessage) *sse.Reader {
inputStr, _ := sonic.MarshalString(input)
req := &workflow.ChatFlowRunRequest{
WorkflowID: wfID,
Parameters: ptr.Of(inputStr),
AdditionalMessages: additionalMessage,
}
if cID != nil {
req.ConversationID = cID
}
if appID != nil {
req.AppID = appID
}
if botID != nil {
req.BotID = botID
}
m, err := sonic.Marshal(req)
assert.NoError(r.t, err)
c, _ := client.NewClient()
hReq, hResp := protocol.AcquireRequest(), protocol.AcquireResponse()
hReq.SetRequestURI("http://localhost:8888" + "/v1/workflows/chat")
hReq.SetMethod("POST")
hReq.SetBody(m)
hReq.SetHeader("Content-Type", "application/json")
err = c.Do(context.Background(), hReq, hResp)
assert.NoError(r.t, err)
if hResp.StatusCode() != http.StatusOK {
r.t.Errorf("unexpected status code: %d, body: %s", hResp.StatusCode(), string(hResp.Body()))
}
re, err := sse.NewReader(hResp)
assert.NoError(r.t, err)
return re
}
func (r *wfTestRunner) runServer() func() {
go func() {
_ = r.h.Run()
@ -2454,7 +2495,7 @@ func TestStartNodeDefaultValues(t *testing.T) {
result, _ := r.openapiSyncRun(idStr, input)
assert.Equal(t, result, map[string]any{
"ts": "2025-07-09 21:43:34",
"files": "http://imagex.fanlv.fun/tos-cn-i-1heqlfnr21/e81acc11277f421390770618e24e01ce.jpeg~tplv-1heqlfnr21-image.image?x-wf-file_name=20250317-154742.jpeg",
"files": "http://imagex.fanlv.fun/tos-cn-i-1heqlfnr21/e81acc11277f421390770618e24e01ce.jpeg~tplv-1heqlfnr21-image.image",
"str": "str",
"object": map[string]any{
"a": "1",
@ -2478,7 +2519,7 @@ func TestStartNodeDefaultValues(t *testing.T) {
result, _ := r.openapiSyncRun(idStr, input)
assert.Equal(t, result, map[string]any{
"ts": "2025-07-09 21:43:34",
"files": "http://imagex.fanlv.fun/tos-cn-i-1heqlfnr21/e81acc11277f421390770618e24e01ce.jpeg~tplv-1heqlfnr21-image.image?x-wf-file_name=20250317-154742.jpeg",
"files": "http://imagex.fanlv.fun/tos-cn-i-1heqlfnr21/e81acc11277f421390770618e24e01ce.jpeg~tplv-1heqlfnr21-image.image",
"str": "str",
"object": map[string]any{
"a": "1",
@ -2503,7 +2544,7 @@ func TestStartNodeDefaultValues(t *testing.T) {
result, _ := r.openapiSyncRun(idStr, input)
assert.Equal(t, result, map[string]any{
"ts": "2025-07-09 21:43:34",
"files": "http://imagex.fanlv.fun/tos-cn-i-1heqlfnr21/e81acc11277f421390770618e24e01ce.jpeg~tplv-1heqlfnr21-image.image?x-wf-file_name=20250317-154742.jpeg",
"files": "http://imagex.fanlv.fun/tos-cn-i-1heqlfnr21/e81acc11277f421390770618e24e01ce.jpeg~tplv-1heqlfnr21-image.image",
"str": "value",
"object": map[string]any{
"a": "1",
@ -5491,7 +5532,7 @@ func TestConversationOfChatFlow(t *testing.T) {
if err != nil {
return err
}
if v.Name == "CONVERSATION_NAME" {
if v.Name == vo.ConversationNameKey {
v.DefaultValue = cName
}
startNode.Data.Outputs[idx] = v
@ -5522,7 +5563,7 @@ func TestConversationOfChatFlow(t *testing.T) {
for _, vAny := range node.Data.Outputs {
v, err := vo.ParseVariable(vAny)
assert.NoError(t, err)
if v.Name == "CONVERSATION_NAME" {
if v.Name == vo.ConversationNameKey {
assert.Equal(t, v.DefaultValue, updateName)
}
}
@ -5569,7 +5610,7 @@ func TestConversationOfChatFlow(t *testing.T) {
for _, vAny := range node.Data.Outputs {
v, err := vo.ParseVariable(vAny)
assert.NoError(t, err)
if v.Name == "CONVERSATION_NAME" {
if v.Name == vo.ConversationNameKey {
assert.Equal(t, v.DefaultValue, cName+"copy")
}
}
@ -5988,3 +6029,266 @@ func TestConversationHistoryNodes(t *testing.T) {
assert.Equal(t, []any{}, outputMap["history_list"])
})
}
func TestWorkflowRunWithFiles(t *testing.T) {
mockey.PatchConvey("workflow run with files", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
r.knowledge.EXPECT().Store(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, document *knowledge.CreateDocumentRequest) (*knowledge.CreateDocumentResponse, error) {
assert.Equal(t, "北京旅游景点.txt", document.FileName)
return &knowledge.CreateDocumentResponse{
DocumentID: 1,
FileURL: document.FileURL,
FileName: document.FileName,
}, nil
}).AnyTimes()
runner := mockcode.NewMockRunner(r.ctrl)
runner.EXPECT().Run(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, request *coderunner.RunRequest) (*coderunner.RunResponse, error) {
return &coderunner.RunResponse{
Result: request.Params,
}, nil
}).AnyTimes()
mockey.Mock(code.GetCodeRunner).Return(runner).Build()
idStr := r.load("workflow_wf_file_name.json")
r.publish(idStr, "v0.1.1", true)
m, execID := r.openapiSyncRun(idStr, map[string]string{
"f": "http://coze.fanlv.fun:8889/opencoze/tos-cn-i-v4nquku3lp/27b01dd5-b0f5-4dbd-a075-a48c14162d23.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20250910%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250910T074412Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=2f3051a0645c9ed260f7cb6c93954147ceb347a61366c9f70b98d43c299a7732&x-wf-file_name=%E5%8C%97%E4%BA%AC%E6%97%85%E6%B8%B8%E6%99%AF%E7%82%B9.txt",
"fs": "[\"http://coze.fanlv.fun:8889/opencoze/tos-cn-i-v4nquku3lp/85056c12-ea40-4588-a2a2-5eab56b94e4c.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20250910%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250910T074404Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=59f3a7b6774a33de127e42878e4821635ce74e1fc29237ba03b13d67a068fedf&x-wf-file_name=%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_2025-07-02_154139_105.jpg\",\"http://coze.fanlv.fun:8889/opencoze/tos-cn-i-v4nquku3lp/5ec9856d-0db0-44a1-9b82-43628221a928.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20250910%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250910T074410Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=3887b0583084b0294b91e93e307c61ce3b910531d0e33a08e7c7d57de24c71ec&x-wf-file_name=20250317-154742.jpeg\"]",
})
assert.NotNil(t, execID)
assert.Equal(t, m["output"], []any{
"http://coze.fanlv.fun:8889/opencoze/tos-cn-i-v4nquku3lp/85056c12-ea40-4588-a2a2-5eab56b94e4c.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20250910%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250910T074404Z&X-Amz-Expires=604800&X-Amz-Signature=59f3a7b6774a33de127e42878e4821635ce74e1fc29237ba03b13d67a068fedf&X-Amz-SignedHeaders=host",
"http://coze.fanlv.fun:8889/opencoze/tos-cn-i-v4nquku3lp/5ec9856d-0db0-44a1-9b82-43628221a928.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20250910%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250910T074410Z&X-Amz-Expires=604800&X-Amz-Signature=3887b0583084b0294b91e93e307c61ce3b910531d0e33a08e7c7d57de24c71ec&X-Amz-SignedHeaders=host"})
assert.Equal(t, m["filename"], "北京旅游景点.txt")
fmt.Println(m, execID)
})
}
func TestChatFlowRun(t *testing.T) {
mockey.PatchConvey("chat flow run", t, func() {
r := newWfTestRunner(t)
appworkflow.SVC.IDGenerator = r.idGen
defer r.closeFn()
defer r.runServer()()
chatModel1 := &testutil.UTChatModel{
StreamResultProvider: func(_ int, in []*schema.Message) (*schema.StreamReader[*schema.Message], error) {
sr := schema.StreamReaderFromArray([]*schema.Message{
{
Role: schema.Assistant,
Content: "I ",
},
{
Role: schema.Assistant,
Content: "don't know.",
},
})
return sr, nil
},
}
r.modelManage.EXPECT().GetModel(gomock.Any(), gomock.Any()).Return(chatModel1, nil, nil).AnyTimes()
id := r.load("chatflow/llm_chat.json", withMode(workflow.WorkflowMode_ChatFlow))
r.publish(id, "v0.0.1", true)
cID := time.Now().UnixNano()
cIDStr := strconv.FormatInt(cID, 10)
appID := time.Now().UnixNano()
appIDStr := strconv.FormatInt(appID, 10)
// Create conversation first
r.conversation.EXPECT().CreateConversation(gomock.Any(), gomock.Any()).Return(&conventity.Conversation{
ID: cID,
}, nil).AnyTimes()
idStr := r.load("conversation_manager/update_dynamic_conversation.json")
r.publish(idStr, "v0.0.1", true)
ret, _ := r.openapiSyncRun(idStr, map[string]string{
"input": "v1",
"new_name": "v2",
}, withRunProjectID(appID))
assert.Equal(t, map[string]any{"conversationId": strconv.FormatInt(cID, 10), "isExisted": false, "isSuccess": true}, ret["obj"])
msg := []*workflow.EnterMessage{
{
Role: "user",
ContentType: "text",
Content: "你好",
},
}
sID := time.Now().UnixNano()
r.conversation.EXPECT().GetByID(gomock.Any(), gomock.Any()).Return(&conventity.Conversation{
ID: cID,
SectionID: sID,
}, nil).AnyTimes()
rID := time.Now().UnixNano()
r.agentRun.EXPECT().Create(gomock.Any(), gomock.Any()).Return(&agententity.RunRecordMeta{
ID: rID,
}, nil).AnyTimes()
mID := time.Now().Unix()
r.message.EXPECT().Create(gomock.Any(), gomock.Any()).Return(&message.Message{
ID: mID,
}, nil).AnyTimes()
t.Run("chat flow run in app", func(t *testing.T) {
sseReader := r.openapiChatFlowRun(id, ptr.Of(cIDStr), ptr.Of(appIDStr), nil, map[string]any{
vo.ConversationNameKey: "Default",
}, msg)
err := sseReader.ForEach(t.Context(), func(e *sse.Event) error {
t.Logf("sse id: %s, type: %s, data: %s", e.ID, e.Type, string(e.Data))
return nil
})
assert.NoError(t, err)
})
t.Run("chat flow run in bot", func(t *testing.T) {
botID := time.Now().UnixNano()
botIDStr := strconv.FormatInt(botID, 10)
sseReader := r.openapiChatFlowRun(id, ptr.Of(cIDStr), nil, ptr.Of(botIDStr), map[string]any{
vo.ConversationNameKey: "Default",
}, msg)
err := sseReader.ForEach(t.Context(), func(e *sse.Event) error {
t.Logf("sse id: %s, type: %s, data: %s", e.ID, e.Type, string(e.Data))
return nil
})
assert.NoError(t, err)
})
t.Run("chat flow run without cID", func(t *testing.T) {
sseReader := r.openapiChatFlowRun(id, nil, ptr.Of(appIDStr), nil, map[string]any{
vo.ConversationNameKey: "Default",
}, msg)
err := sseReader.ForEach(t.Context(), func(e *sse.Event) error {
t.Logf("sse id: %s, type: %s, data: %s", e.ID, e.Type, string(e.Data))
return nil
})
assert.NoError(t, err)
})
t.Run("chat flow run with additional messages", func(t *testing.T) {
additionalMsg := []*workflow.EnterMessage{
{
Role: "user",
ContentType: "text",
Content: "你好, 我叫小明",
},
{
Role: "assistant",
ContentType: "text",
Content: "你好小明, 很高兴认识你",
},
{
Role: "user",
ContentType: "text",
Content: "你好",
},
}
sseReader := r.openapiChatFlowRun(id, ptr.Of(cIDStr), ptr.Of(appIDStr), nil, map[string]any{
vo.ConversationNameKey: "Default",
}, additionalMsg)
err := sseReader.ForEach(t.Context(), func(e *sse.Event) error {
t.Logf("sse id: %s, type: %s, data: %s", e.ID, e.Type, string(e.Data))
return nil
})
assert.NoError(t, err)
})
t.Run("chat flow run with history messages", func(t *testing.T) {
id := r.load("chatflow/llm_chat_with_history.json", withMode(workflow.WorkflowMode_ChatFlow))
r.publish(id, "v0.0.1", true)
r.message.EXPECT().GetLatestRunIDs(gomock.Any(), gomock.Any()).Return([]int64{rID}, nil).AnyTimes()
r.message.EXPECT().GetMessagesByRunIDs(gomock.Any(), gomock.Any()).Return(&message0.GetMessagesByRunIDsResponse{
Messages: []*message0.WfMessage{
{
ID: mID,
Role: schema.User,
Text: ptr.Of("你好"),
},
},
}, nil).AnyTimes()
sseReader := r.openapiChatFlowRun(id, ptr.Of(cIDStr), ptr.Of(appIDStr), nil, map[string]any{
vo.ConversationNameKey: "Default",
}, msg)
err := sseReader.ForEach(t.Context(), func(e *sse.Event) error {
t.Logf("sse id: %s, type: %s, data: %s", e.ID, e.Type, string(e.Data))
return nil
})
assert.NoError(t, err)
})
t.Run("chat flow run with interrupt nodes ", func(t *testing.T) {
// 生成一个携带 input, 问答文本 问答选项的三个中断节点 做测试
id := r.load("chatflow/chat_run_with_interrupt.json", withMode(workflow.WorkflowMode_ChatFlow))
r.publish(id, "v0.0.1", true)
sseReader := r.openapiChatFlowRun(id, ptr.Of(cIDStr), ptr.Of(appIDStr), nil, map[string]any{
vo.ConversationNameKey: "Default",
}, msg)
err := sseReader.ForEach(t.Context(), func(e *sse.Event) error {
t.Logf("sse id: %s, type: %s, data: %s", e.ID, e.Type, string(e.Data))
if e.ID == "3" {
assert.Equal(t, e.Type, "conversation.message.completed")
assert.Contains(t, string(e.Data), "7383997384420262000")
}
return nil
})
assert.NoError(t, err)
sseReader = r.openapiChatFlowRun(id, ptr.Of(cIDStr), ptr.Of(appIDStr), nil, map[string]any{
vo.ConversationNameKey: "Default",
}, []*workflow.EnterMessage{
{Role: string(schema.User), Content: "input:1", ContentType: "text"},
})
err = sseReader.ForEach(t.Context(), func(e *sse.Event) error {
t.Logf("sse id: %s, type: %s, data: %s", e.ID, e.Type, string(e.Data))
if e.ID == "4" {
assert.Equal(t, e.Type, "conversation.message.completed")
assert.Contains(t, string(e.Data), "你好")
}
return nil
})
assert.NoError(t, err)
sseReader = r.openapiChatFlowRun(id, ptr.Of(cIDStr), ptr.Of(appIDStr), nil, map[string]any{
vo.ConversationNameKey: "Default",
}, []*workflow.EnterMessage{
{Role: string(schema.User), Content: "hello", ContentType: "text"},
})
err = sseReader.ForEach(t.Context(), func(e *sse.Event) error {
if e.ID == "3" {
assert.Equal(t, e.Type, "conversation.message.completed")
assert.Contains(t, string(e.Data), "question_card_data", "请选择")
}
return nil
})
assert.NoError(t, err)
sseReader = r.openapiChatFlowRun(id, ptr.Of(cIDStr), ptr.Of(appIDStr), nil, map[string]any{
vo.ConversationNameKey: "Default",
}, []*workflow.EnterMessage{
{Role: string(schema.User), Content: "A", ContentType: "text"},
})
err = sseReader.ForEach(t.Context(), func(e *sse.Event) error {
t.Logf("sse id: %s, type: %s, data: %s", e.ID, e.Type, string(e.Data))
if e.ID == "4" {
assert.Equal(t, e.Type, "conversation.message.completed")
assert.Contains(t, string(e.Data), "answer", "A")
}
return nil
})
assert.NoError(t, err)
})
})
}

View File

@ -56,6 +56,7 @@ type ExecuteConfig struct {
ConversationHistorySchemaMessages []*schema.Message
SectionID *int64
MaxHistoryRounds *int32
InputFileFields map[string]*FileInfo
}
type ExecuteMode string
@ -91,3 +92,9 @@ const (
BizTypeAgent BizType = "agent"
BizTypeWorkflow BizType = "workflow"
)
type FileInfo struct {
FileURL string `json:"file_url"`
FileName string `json:"file_name"`
FileExtension string `json:"file_extension"`
}

View File

@ -51,7 +51,6 @@ import (
"github.com/coze-dev/coze-studio/backend/infra/contract/messages2query"
"github.com/coze-dev/coze-studio/backend/infra/contract/modelmgr"
"github.com/coze-dev/coze-studio/backend/infra/impl/cache/redis"
"github.com/coze-dev/coze-studio/backend/infra/impl/coderunner/direct"
"github.com/coze-dev/coze-studio/backend/infra/impl/coderunner/sandbox"
builtinNL2SQL "github.com/coze-dev/coze-studio/backend/infra/impl/document/nl2sql/builtin"
"github.com/coze-dev/coze-studio/backend/infra/impl/document/ocr/ppocr"
@ -346,40 +345,43 @@ func initKnowledgeEventBusProducer() (eventbus.Producer, error) {
}
func initCodeRunner() coderunner.Runner {
switch typ := os.Getenv(consts.CodeRunnerType); typ {
case "sandbox":
getAndSplit := func(key string) []string {
v := os.Getenv(key)
if v == "" {
return nil
}
return strings.Split(v, ",")
// 为了安全考虑移除不安全的direct runner强制使用sandbox
getAndSplit := func(key string) []string {
v := os.Getenv(key)
if v == "" {
return nil
}
config := &sandbox.Config{
AllowEnv: getAndSplit(consts.CodeRunnerAllowEnv),
AllowRead: getAndSplit(consts.CodeRunnerAllowRead),
AllowWrite: getAndSplit(consts.CodeRunnerAllowWrite),
AllowNet: getAndSplit(consts.CodeRunnerAllowNet),
AllowRun: getAndSplit(consts.CodeRunnerAllowRun),
AllowFFI: getAndSplit(consts.CodeRunnerAllowFFI),
NodeModulesDir: os.Getenv(consts.CodeRunnerNodeModulesDir),
TimeoutSeconds: 0,
MemoryLimitMB: 0,
}
if f, err := strconv.ParseFloat(os.Getenv(consts.CodeRunnerTimeoutSeconds), 64); err == nil {
config.TimeoutSeconds = f
} else {
config.TimeoutSeconds = 60.0
}
if mem, err := strconv.ParseInt(os.Getenv(consts.CodeRunnerMemoryLimitMB), 10, 64); err == nil {
config.MemoryLimitMB = mem
} else {
config.MemoryLimitMB = 100
}
return sandbox.NewRunner(config)
default:
return direct.NewRunner()
return strings.Split(v, ",")
}
// 使用安全的默认配置
config := &sandbox.Config{
AllowEnv: getAndSplit(consts.CodeRunnerAllowEnv), // 默认为空,禁止环境变量访问
AllowRead: getAndSplit(consts.CodeRunnerAllowRead), // 默认为空,禁止文件读取
AllowWrite: getAndSplit(consts.CodeRunnerAllowWrite), // 默认为空,禁止文件写入
AllowNet: getAndSplit(consts.CodeRunnerAllowNet), // 默认为空,禁止网络访问
AllowRun: getAndSplit(consts.CodeRunnerAllowRun), // 默认为空,禁止运行外部程序
AllowFFI: getAndSplit(consts.CodeRunnerAllowFFI), // 默认为空禁止FFI调用
NodeModulesDir: os.Getenv(consts.CodeRunnerNodeModulesDir),
TimeoutSeconds: 0,
MemoryLimitMB: 0,
}
// 设置安全的超时时间最大30秒
if f, err := strconv.ParseFloat(os.Getenv(consts.CodeRunnerTimeoutSeconds), 64); err == nil && f > 0 && f <= 30 {
config.TimeoutSeconds = f
} else {
config.TimeoutSeconds = 30.0 // 默认30秒超时
}
// 设置安全的内存限制最大100MB
if mem, err := strconv.ParseInt(os.Getenv(consts.CodeRunnerMemoryLimitMB), 10, 64); err == nil && mem > 0 && mem <= 100 {
config.MemoryLimitMB = mem
} else {
config.MemoryLimitMB = 100 // 默认100MB内存限制
}
return sandbox.NewRunner(config)
}
func initOCR() ocr.OCR {
@ -798,4 +800,4 @@ func getEmbedding(ctx context.Context) (embedding.Embedder, error) {
}
return emb, nil
}
}

View File

@ -209,7 +209,7 @@ type publishFn func(ctx context.Context, appContext *ServiceComponents, publishI
func publishAgentVariables(ctx context.Context, appContext *ServiceComponents, publishInfo *entity.SingleAgentPublish, agent *entity.SingleAgent) (*entity.SingleAgent, error) {
draftAgent := agent
if draftAgent.VariablesMetaID != nil || *draftAgent.VariablesMetaID == 0 {
if draftAgent.VariablesMetaID == nil || *draftAgent.VariablesMetaID == 0 {
return draftAgent, nil
}

View File

@ -221,7 +221,7 @@ const (
"id": "5fJt3qKpSz",
"name": "list",
"defaultValue": [
]
}
},
@ -504,7 +504,7 @@ func (w *ApplicationService) OpenAPIChatFlowRun(ctx context.Context, req *workfl
workflowID = mustParseInt64(req.GetWorkflowID())
isDebug = req.GetExecuteMode() == "DEBUG"
appID, agentID *int64
resolveAppID int64
bizID int64
conversationID int64
sectionID int64
version string
@ -521,11 +521,11 @@ func (w *ApplicationService) OpenAPIChatFlowRun(ctx context.Context, req *workfl
if req.IsSetAppID() {
appID = ptr.Of(mustParseInt64(req.GetAppID()))
resolveAppID = mustParseInt64(req.GetAppID())
bizID = mustParseInt64(req.GetAppID())
}
if req.IsSetBotID() {
agentID = ptr.Of(mustParseInt64(req.GetBotID()))
resolveAppID = mustParseInt64(req.GetBotID())
bizID = mustParseInt64(req.GetBotID())
}
if appID != nil && agentID != nil {
@ -564,16 +564,16 @@ func (w *ApplicationService) OpenAPIChatFlowRun(ctx context.Context, req *workfl
sectionID = cInfo.SectionID
// only trust the conversation name under the app
conversationName, existed, err := GetWorkflowDomainSVC().GetConversationNameByID(ctx, ternary.IFElse(isDebug, vo.Draft, vo.Online), resolveAppID, connectorID, conversationID)
conversationName, existed, err := GetWorkflowDomainSVC().GetConversationNameByID(ctx, ternary.IFElse(isDebug, vo.Draft, vo.Online), bizID, connectorID, conversationID)
if err != nil {
return nil, err
}
if !existed {
return nil, fmt.Errorf("conversation not found")
}
parameters["CONVERSATION_NAME"] = conversationName
parameters[vo.ConversationNameKey] = conversationName
} else if req.IsSetConversationID() && req.IsSetBotID() {
parameters["CONVERSATION_NAME"] = "Default"
parameters[vo.ConversationNameKey] = "Default"
conversationID = mustParseInt64(req.GetConversationID())
cInfo, err := crossconversation.DefaultSVC().GetByID(ctx, conversationID)
if err != nil {
@ -581,11 +581,11 @@ func (w *ApplicationService) OpenAPIChatFlowRun(ctx context.Context, req *workfl
}
sectionID = cInfo.SectionID
} else {
conversationName, ok := parameters["CONVERSATION_NAME"].(string)
conversationName, ok := parameters[vo.ConversationNameKey].(string)
if !ok {
return nil, fmt.Errorf("conversation name is requried")
}
cID, sID, err := GetWorkflowDomainSVC().GetOrCreateConversation(ctx, ternary.IFElse(isDebug, vo.Draft, vo.Online), resolveAppID, connectorID, userID, conversationName)
cID, sID, err := GetWorkflowDomainSVC().GetOrCreateConversation(ctx, ternary.IFElse(isDebug, vo.Draft, vo.Online), bizID, connectorID, userID, conversationName)
if err != nil {
return nil, err
}
@ -594,7 +594,7 @@ func (w *ApplicationService) OpenAPIChatFlowRun(ctx context.Context, req *workfl
}
runRecord, err := crossagentrun.DefaultSVC().Create(ctx, &agententity.AgentRunMeta{
AgentID: resolveAppID,
AgentID: bizID,
ConversationID: conversationID,
UserID: strconv.FormatInt(userID, 10),
ConnectorID: connectorID,
@ -606,7 +606,7 @@ func (w *ApplicationService) OpenAPIChatFlowRun(ctx context.Context, req *workfl
roundID := runRecord.ID
userMessage, err := toConversationMessage(ctx, resolveAppID, conversationID, userID, roundID, sectionID, message.MessageTypeQuestion, lastUserMessage)
userMessage, err := toConversationMessage(ctx, bizID, conversationID, userID, roundID, sectionID, message.MessageTypeQuestion, lastUserMessage)
if err != nil {
return nil, err
}
@ -648,7 +648,7 @@ func (w *ApplicationService) OpenAPIChatFlowRun(ctx context.Context, req *workfl
return nil, err
}
return schema.StreamReaderWithConvert(sr, w.convertToChatFlowRunResponseList(ctx, convertToChatFlowInfo{
appID: resolveAppID,
bizID: bizID,
conversationID: conversationID,
roundID: roundID,
workflowID: workflowID,
@ -684,7 +684,7 @@ func (w *ApplicationService) OpenAPIChatFlowRun(ctx context.Context, req *workfl
Cancellable: isDebug,
}
historyMessages, err := makeChatFlowHistoryMessages(ctx, resolveAppID, conversationID, userID, sectionID, connectorID, messages[:len(req.GetAdditionalMessages())-1])
historyMessages, err := makeChatFlowHistoryMessages(ctx, bizID, conversationID, userID, sectionID, connectorID, messages[:len(req.GetAdditionalMessages())-1])
if err != nil {
return nil, err
}
@ -706,7 +706,7 @@ func (w *ApplicationService) OpenAPIChatFlowRun(ctx context.Context, req *workfl
logs.CtxWarnf(ctx, "create history message failed, err=%v", err)
}
}
parameters["USER_INPUT"], err = w.makeChatFlowUserInput(ctx, lastUserMessage)
parameters[vo.UserInputKey], err = w.makeChatFlowUserInput(ctx, lastUserMessage)
if err != nil {
return nil, err
}
@ -717,7 +717,7 @@ func (w *ApplicationService) OpenAPIChatFlowRun(ctx context.Context, req *workfl
}
return schema.StreamReaderWithConvert(sr, w.convertToChatFlowRunResponseList(ctx, convertToChatFlowInfo{
appID: resolveAppID,
bizID: bizID,
conversationID: conversationID,
roundID: roundID,
workflowID: workflowID,
@ -731,7 +731,7 @@ func (w *ApplicationService) OpenAPIChatFlowRun(ctx context.Context, req *workfl
func (w *ApplicationService) convertToChatFlowRunResponseList(ctx context.Context, info convertToChatFlowInfo) func(msg *entity.Message) (responses []*workflow.ChatFlowRunResponse, err error) {
var (
appID = info.appID
bizID = info.bizID
conversationID = info.conversationID
roundID = info.roundID
workflowID = info.workflowID
@ -798,7 +798,7 @@ func (w *ApplicationService) convertToChatFlowRunResponseList(ctx context.Contex
ChatID: strconv.FormatInt(roundID, 10),
ConversationID: strconv.FormatInt(conversationID, 10),
SectionID: strconv.FormatInt(sectionID, 10),
BotID: strconv.FormatInt(appID, 10),
BotID: strconv.FormatInt(bizID, 10),
Role: string(schema.Assistant),
Type: "follow_up",
ContentType: "text",
@ -815,7 +815,7 @@ func (w *ApplicationService) convertToChatFlowRunResponseList(ctx context.Contex
ID: strconv.FormatInt(roundID, 10),
ConversationID: strconv.FormatInt(conversationID, 10),
SectionID: strconv.FormatInt(sectionID, 10),
BotID: strconv.FormatInt(appID, 10),
BotID: strconv.FormatInt(bizID, 10),
Status: vo.Completed,
ExecuteID: strconv.FormatInt(executeID, 10),
Usage: &vo.Usage{
@ -929,7 +929,7 @@ func (w *ApplicationService) convertToChatFlowRunResponseList(ctx context.Contex
}
_, err = crossmessage.DefaultSVC().Create(ctx, &message.Message{
AgentID: appID,
AgentID: bizID,
RunID: roundID,
SectionID: sectionID,
Content: msgContent,
@ -947,7 +947,7 @@ func (w *ApplicationService) convertToChatFlowRunResponseList(ctx context.Contex
ChatID: strconv.FormatInt(roundID, 10),
ConversationID: strconv.FormatInt(conversationID, 10),
SectionID: strconv.FormatInt(sectionID, 10),
BotID: strconv.FormatInt(appID, 10),
BotID: strconv.FormatInt(bizID, 10),
Role: string(schema.Assistant),
Type: string(entity.Answer),
ContentType: string(contentType),
@ -1046,7 +1046,7 @@ func (w *ApplicationService) convertToChatFlowRunResponseList(ctx context.Contex
}
intermediateMessage = &message.Message{
ID: id,
AgentID: appID,
AgentID: bizID,
RunID: roundID,
SectionID: sectionID,
ConversationID: conversationID,
@ -1066,7 +1066,7 @@ func (w *ApplicationService) convertToChatFlowRunResponseList(ctx context.Contex
ChatID: strconv.FormatInt(roundID, 10),
ConversationID: strconv.FormatInt(conversationID, 10),
SectionID: strconv.FormatInt(sectionID, 10),
BotID: strconv.FormatInt(appID, 10),
BotID: strconv.FormatInt(bizID, 10),
Role: string(dataMessage.Role),
Type: string(dataMessage.Type),
ContentType: string(message.ContentTypeText),
@ -1092,7 +1092,7 @@ func (w *ApplicationService) convertToChatFlowRunResponseList(ctx context.Contex
ChatID: strconv.FormatInt(roundID, 10),
ConversationID: strconv.FormatInt(conversationID, 10),
SectionID: strconv.FormatInt(sectionID, 10),
BotID: strconv.FormatInt(appID, 10),
BotID: strconv.FormatInt(bizID, 10),
Role: string(dataMessage.Role),
Type: string(dataMessage.Type),
ContentType: string(message.ContentTypeText),
@ -1155,9 +1155,9 @@ func (w *ApplicationService) makeChatFlowUserInput(ctx context.Context, message
} else {
return "", fmt.Errorf("invalid message ccontent type %v", message.ContentType)
}
}
func makeChatFlowHistoryMessages(ctx context.Context, appID, conversationID, userID, sectionID, connectorID int64, messages []*workflow.EnterMessage) ([]*message.Message, error) {
func makeChatFlowHistoryMessages(ctx context.Context, bizID, conversationID, userID, sectionID, connectorID int64, messages []*workflow.EnterMessage) ([]*message.Message, error) {
var (
rID int64
@ -1170,7 +1170,7 @@ func makeChatFlowHistoryMessages(ctx context.Context, appID, conversationID, use
for _, msg := range messages {
if msg.Role == userRole {
runRecord, err = crossagentrun.DefaultSVC().Create(ctx, &agententity.AgentRunMeta{
AgentID: appID,
AgentID: bizID,
ConversationID: conversationID,
UserID: strconv.FormatInt(userID, 10),
ConnectorID: connectorID,
@ -1180,13 +1180,15 @@ func makeChatFlowHistoryMessages(ctx context.Context, appID, conversationID, use
return nil, err
}
rID = runRecord.ID
} else if msg.Role == assistantRole && rID == 0 {
continue
} else if msg.Role == assistantRole {
if rID == 0 {
continue
}
} else {
return nil, fmt.Errorf("invalid role type %v", msg.Role)
}
m, err := toConversationMessage(ctx, appID, conversationID, userID, rID, sectionID, ternary.IFElse(msg.Role == userRole, message.MessageTypeQuestion, message.MessageTypeAnswer), msg)
m, err := toConversationMessage(ctx, bizID, conversationID, userID, rID, sectionID, ternary.IFElse(msg.Role == userRole, message.MessageTypeQuestion, message.MessageTypeAnswer), msg)
if err != nil {
return nil, err
}
@ -1274,7 +1276,7 @@ func (w *ApplicationService) OpenAPICreateConversation(ctx context.Context, req
}, nil
}
func toConversationMessage(ctx context.Context, appID, cid, userID, roundID, sectionID int64, messageType message.MessageType, msg *workflow.EnterMessage) (*message.Message, error) {
func toConversationMessage(ctx context.Context, bizID, cid, userID, roundID, sectionID int64, messageType message.MessageType, msg *workflow.EnterMessage) (*message.Message, error) {
type content struct {
Type string `json:"type"`
FileID *string `json:"file_id"`
@ -1284,7 +1286,7 @@ func toConversationMessage(ctx context.Context, appID, cid, userID, roundID, sec
return &message.Message{
Role: schema.User,
ConversationID: cid,
AgentID: appID,
AgentID: bizID,
RunID: roundID,
Content: msg.Content,
ContentType: message.ContentTypeText,
@ -1304,7 +1306,7 @@ func toConversationMessage(ctx context.Context, appID, cid, userID, roundID, sec
Role: schema.User,
MessageType: messageType,
ConversationID: cid,
AgentID: appID,
AgentID: bizID,
UserID: strconv.FormatInt(userID, 10),
RunID: roundID,
ContentType: message.ContentTypeMix,
@ -1432,7 +1434,7 @@ func toSchemaMessage(ctx context.Context, msg *workflow.EnterMessage) (*schema.M
type convertToChatFlowInfo struct {
userMessage *schema.Message
appID int64
bizID int64
conversationID int64
roundID int64
workflowID int64

View File

@ -0,0 +1,604 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package workflow
import (
"context"
"errors"
"strconv"
"testing"
"github.com/cloudwego/eino/schema"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
messageentity "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/message"
"github.com/coze-dev/coze-studio/backend/api/model/workflow"
crossagentrun "github.com/coze-dev/coze-studio/backend/crossdomain/contract/agentrun"
"github.com/coze-dev/coze-studio/backend/crossdomain/contract/agentrun/agentrunmock"
crossupload "github.com/coze-dev/coze-studio/backend/crossdomain/contract/upload"
"github.com/coze-dev/coze-studio/backend/crossdomain/contract/upload/uploadmock"
agententity "github.com/coze-dev/coze-studio/backend/domain/conversation/agentrun/entity"
uploadentity "github.com/coze-dev/coze-studio/backend/domain/upload/entity"
"github.com/coze-dev/coze-studio/backend/domain/upload/service"
)
func TestApplicationService_makeChatFlowUserInput(t *testing.T) {
ctx := context.Background()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockUpload := uploadmock.NewMockUploader(ctrl)
crossupload.SetDefaultSVC(mockUpload)
tests := []struct {
name string
message *workflow.EnterMessage
setupMock func()
expected string
expectErr bool
}{
{
name: "content type text",
message: &workflow.EnterMessage{
ContentType: "text",
Content: "hello",
},
setupMock: func() {},
expected: "hello",
expectErr: false,
},
{
name: "content type object_string with text",
message: &workflow.EnterMessage{
ContentType: "object_string",
Content: `[{"type": "text", "text": "hello world"}]`,
},
setupMock: func() {},
expected: "hello world",
expectErr: false,
},
{
name: "content type object_string with file",
message: &workflow.EnterMessage{
ContentType: "object_string",
Content: `[{"type": "file", "file_id": "123"}]`,
},
setupMock: func() {
mockUpload.EXPECT().GetFile(gomock.Any(), &service.GetFileRequest{ID: 123}).Return(&service.GetFileResponse{
File: &uploadentity.File{Url: "https://example.com/file"},
}, nil)
},
expected: "https://example.com/file",
expectErr: false,
},
{
name: "content type object_string with text and file",
message: &workflow.EnterMessage{
ContentType: "object_string",
Content: `[{"type": "text", "text": "see this file"}, {"type": "file", "file_id": "123"}]`,
},
setupMock: func() {
mockUpload.EXPECT().GetFile(gomock.Any(), &service.GetFileRequest{ID: 123}).Return(&service.GetFileResponse{
File: &uploadentity.File{Url: "https://example.com/file"},
}, nil)
},
expected: "see this file,https://example.com/file",
expectErr: false,
},
{
name: "get file error",
message: &workflow.EnterMessage{
ContentType: "object_string",
Content: `[{"type": "file", "file_id": "123"}]`,
},
setupMock: func() {
mockUpload.EXPECT().GetFile(gomock.Any(), &service.GetFileRequest{ID: 123}).Return(nil, errors.New("get file error"))
},
expectErr: true,
},
{
name: "file not found",
message: &workflow.EnterMessage{
ContentType: "object_string",
Content: `[{"type": "file", "file_id": "123"}]`,
},
setupMock: func() {
mockUpload.EXPECT().GetFile(gomock.Any(), &service.GetFileRequest{ID: 123}).Return(&service.GetFileResponse{
File: nil,
}, nil)
},
expectErr: true,
},
{
name: "invalid content type",
message: &workflow.EnterMessage{
ContentType: "invalid",
},
setupMock: func() {},
expectErr: true,
},
{
name: "invalid json",
message: &workflow.EnterMessage{
ContentType: "object_string",
Content: `invalid-json`,
},
setupMock: func() {},
expectErr: true,
},
}
w := &ApplicationService{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setupMock()
result, err := w.makeChatFlowUserInput(ctx, tt.message)
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}
func Test_toConversationMessage(t *testing.T) {
ctx := context.Background()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockUpload := uploadmock.NewMockUploader(ctrl)
crossupload.SetDefaultSVC(mockUpload)
bizID, cid, userID, roundID, sectionID := int64(2), int64(1), int64(4), int64(3), int64(5)
tests := []struct {
name string
msg *workflow.EnterMessage
messageType messageentity.MessageType
setupMock func()
expected *messageentity.Message
expectErr bool
}{
{
name: "content type text",
msg: &workflow.EnterMessage{
ContentType: "text",
Content: "hello",
},
messageType: messageentity.MessageTypeQuestion,
setupMock: func() {},
expected: &messageentity.Message{
Role: schema.User,
ConversationID: cid,
AgentID: bizID,
RunID: roundID,
Content: "hello",
ContentType: messageentity.ContentTypeText,
MessageType: messageentity.MessageTypeQuestion,
UserID: strconv.FormatInt(userID, 10),
SectionID: sectionID,
},
expectErr: false,
},
{
name: "content type object_string with text",
msg: &workflow.EnterMessage{
ContentType: "object_string",
Content: `[{"type": "text", "text": "hello"}]`,
},
messageType: messageentity.MessageTypeQuestion,
setupMock: func() {},
expected: &messageentity.Message{
Role: schema.User,
MessageType: messageentity.MessageTypeQuestion,
ConversationID: cid,
AgentID: bizID,
UserID: strconv.FormatInt(userID, 10),
RunID: roundID,
ContentType: messageentity.ContentTypeMix,
MultiContent: []*messageentity.InputMetaData{
{Type: messageentity.InputTypeText, Text: "hello"},
},
SectionID: sectionID,
},
expectErr: false,
},
{
name: "content type object_string with file",
msg: &workflow.EnterMessage{
ContentType: "object_string",
Content: `[{"type": "file", "file_id": "123"}]`,
},
messageType: messageentity.MessageTypeQuestion,
setupMock: func() {
mockUpload.EXPECT().GetFile(gomock.Any(), &service.GetFileRequest{ID: 123}).Return(&service.GetFileResponse{
File: &uploadentity.File{Url: "https://example.com/file", TosURI: "tos://uri", Name: "file.txt"},
}, nil)
},
expected: &messageentity.Message{
Role: schema.User,
MessageType: messageentity.MessageTypeQuestion,
ConversationID: cid,
AgentID: bizID,
UserID: strconv.FormatInt(userID, 10),
RunID: roundID,
ContentType: messageentity.ContentTypeMix,
MultiContent: []*messageentity.InputMetaData{
{
Type: "file",
FileData: []*messageentity.FileData{
{Url: "https://example.com/file", URI: "tos://uri", Name: "file.txt"},
},
},
},
SectionID: sectionID,
},
expectErr: false,
},
{
name: "get file error",
msg: &workflow.EnterMessage{
ContentType: "object_string",
Content: `[{"type": "file", "file_id": "123"}]`,
},
setupMock: func() {
mockUpload.EXPECT().GetFile(gomock.Any(), &service.GetFileRequest{ID: 123}).Return(nil, errors.New("get file error"))
},
expectErr: true,
},
{
name: "file not found",
msg: &workflow.EnterMessage{
ContentType: "object_string",
Content: `[{"type": "file", "file_id": "123"}]`,
},
setupMock: func() {
mockUpload.EXPECT().GetFile(gomock.Any(), &service.GetFileRequest{ID: 123}).Return(&service.GetFileResponse{}, nil)
},
expectErr: true,
},
{
name: "invalid content type",
msg: &workflow.EnterMessage{
ContentType: "invalid",
},
setupMock: func() {},
expectErr: true,
},
{
name: "invalid json",
msg: &workflow.EnterMessage{
ContentType: "object_string",
Content: "invalid-json",
},
setupMock: func() {},
expectErr: true,
},
{
name: "invalid input type",
msg: &workflow.EnterMessage{
ContentType: "object_string",
Content: `[{"type": "invalid"}]`,
},
setupMock: func() {},
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setupMock()
result, err := toConversationMessage(ctx, bizID, cid, userID, roundID, sectionID, tt.messageType, tt.msg)
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}
func Test_toSchemaMessage(t *testing.T) {
ctx := context.Background()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockUpload := uploadmock.NewMockUploader(ctrl)
crossupload.SetDefaultSVC(mockUpload)
tests := []struct {
name string
msg *workflow.EnterMessage
setupMock func()
expected *schema.Message
expectErr bool
}{
{
name: "content type text",
msg: &workflow.EnterMessage{
ContentType: "text",
Content: "hello",
},
setupMock: func() {},
expected: &schema.Message{
Role: schema.User,
Content: "hello",
},
expectErr: false,
},
{
name: "content type object_string with text",
msg: &workflow.EnterMessage{
ContentType: "object_string",
Content: `[{"type": "text", "text": "hello"}]`,
},
setupMock: func() {},
expected: &schema.Message{
Role: schema.User,
MultiContent: []schema.ChatMessagePart{
{Type: schema.ChatMessagePartTypeText, Text: "hello"},
},
},
expectErr: false,
},
{
name: "content type object_string with image",
msg: &workflow.EnterMessage{
ContentType: "object_string",
Content: `[{"type": "image", "file_id": "123"}]`,
},
setupMock: func() {
mockUpload.EXPECT().GetFile(gomock.Any(), &service.GetFileRequest{ID: 123}).Return(&service.GetFileResponse{
File: &uploadentity.File{Url: "https://example.com/image.png"},
}, nil)
},
expected: &schema.Message{
Role: schema.User,
MultiContent: []schema.ChatMessagePart{
{
Type: schema.ChatMessagePartTypeImageURL,
ImageURL: &schema.ChatMessageImageURL{URL: "https://example.com/image.png"},
},
},
},
expectErr: false,
},
{
name: "content type object_string with various file types",
msg: &workflow.EnterMessage{
ContentType: "object_string",
Content: `[{"type": "file", "file_id": "1"}, {"type": "audio", "file_id": "2"}, {"type": "video", "file_id": "3"}]`,
},
setupMock: func() {
mockUpload.EXPECT().GetFile(gomock.Any(), &service.GetFileRequest{ID: 1}).Return(&service.GetFileResponse{File: &uploadentity.File{Url: "https://example.com/file"}}, nil)
mockUpload.EXPECT().GetFile(gomock.Any(), &service.GetFileRequest{ID: 2}).Return(&service.GetFileResponse{File: &uploadentity.File{Url: "https://example.com/audio"}}, nil)
mockUpload.EXPECT().GetFile(gomock.Any(), &service.GetFileRequest{ID: 3}).Return(&service.GetFileResponse{File: &uploadentity.File{Url: "https://example.com/video"}}, nil)
},
expected: &schema.Message{
Role: schema.User,
MultiContent: []schema.ChatMessagePart{
{Type: schema.ChatMessagePartTypeFileURL, FileURL: &schema.ChatMessageFileURL{URL: "https://example.com/file"}},
{Type: schema.ChatMessagePartTypeAudioURL, AudioURL: &schema.ChatMessageAudioURL{URL: "https://example.com/audio"}},
{Type: schema.ChatMessagePartTypeVideoURL, VideoURL: &schema.ChatMessageVideoURL{URL: "https://example.com/video"}},
},
},
expectErr: false,
},
{
name: "get file error",
msg: &workflow.EnterMessage{
ContentType: "object_string",
Content: `[{"type": "file", "file_id": "123"}]`,
},
setupMock: func() {
mockUpload.EXPECT().GetFile(gomock.Any(), &service.GetFileRequest{ID: 123}).Return(nil, errors.New("get file error"))
},
expectErr: true,
},
{
name: "file not found",
msg: &workflow.EnterMessage{
ContentType: "object_string",
Content: `[{"type": "file", "file_id": "123"}]`,
},
setupMock: func() {
mockUpload.EXPECT().GetFile(gomock.Any(), &service.GetFileRequest{ID: 123}).Return(&service.GetFileResponse{}, nil)
},
expectErr: true,
},
{
name: "invalid content type",
msg: &workflow.EnterMessage{
ContentType: "invalid",
},
setupMock: func() {},
expectErr: true,
},
{
name: "invalid json",
msg: &workflow.EnterMessage{
ContentType: "object_string",
Content: "invalid-json",
},
setupMock: func() {},
expectErr: true,
},
{
name: "invalid input type",
msg: &workflow.EnterMessage{
ContentType: "object_string",
Content: `[{"type": "invalid"}]`,
},
setupMock: func() {},
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setupMock()
result, err := toSchemaMessage(ctx, tt.msg)
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}
func Test_makeChatFlowHistoryMessages(t *testing.T) {
ctx := context.Background()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockAgentRun := agentrunmock.NewMockAgentRun(ctrl)
crossagentrun.SetDefaultSVC(mockAgentRun)
mockUpload := uploadmock.NewMockUploader(ctrl)
crossupload.SetDefaultSVC(mockUpload)
bizID, conversationID, userID, sectionID, connectorID := int64(2), int64(1), int64(3), int64(4), int64(5)
tests := []struct {
name string
messages []*workflow.EnterMessage
setupMock func()
expected []*messageentity.Message
expectErr bool
}{
{
name: "empty messages",
messages: []*workflow.EnterMessage{},
setupMock: func() {},
expected: []*messageentity.Message{},
expectErr: false,
},
{
name: "one user message",
messages: []*workflow.EnterMessage{
{Role: "user", ContentType: "text", Content: "hello"},
},
setupMock: func() {
mockAgentRun.EXPECT().Create(gomock.Any(), gomock.Any()).Return(&agententity.RunRecordMeta{ID: 100}, nil).Times(1)
},
expected: []*messageentity.Message{
{
Role: schema.User,
ConversationID: conversationID,
AgentID: bizID,
RunID: 100,
Content: "hello",
ContentType: messageentity.ContentTypeText,
MessageType: messageentity.MessageTypeQuestion,
UserID: strconv.FormatInt(userID, 10),
SectionID: sectionID,
},
},
expectErr: false,
},
{
name: "user and assistant message",
messages: []*workflow.EnterMessage{
{Role: "user", ContentType: "text", Content: "hello"},
{Role: "assistant", ContentType: "text", Content: "hi"},
},
setupMock: func() {
mockAgentRun.EXPECT().Create(gomock.Any(), gomock.Any()).Return(&agententity.RunRecordMeta{ID: 100}, nil).Times(1)
},
expected: []*messageentity.Message{
{
Role: schema.User,
ConversationID: conversationID,
AgentID: bizID,
RunID: 100,
Content: "hello",
ContentType: messageentity.ContentTypeText,
MessageType: messageentity.MessageTypeQuestion,
UserID: strconv.FormatInt(userID, 10),
SectionID: sectionID,
},
{
Role: schema.User,
ConversationID: conversationID,
AgentID: bizID,
RunID: 100,
Content: "hi",
ContentType: messageentity.ContentTypeText,
MessageType: messageentity.MessageTypeAnswer,
UserID: strconv.FormatInt(userID, 10),
SectionID: sectionID,
},
},
expectErr: false,
},
{
name: "only assistant message",
messages: []*workflow.EnterMessage{
{Role: "assistant", ContentType: "text", Content: "hi"},
},
setupMock: func() {},
expected: []*messageentity.Message{},
expectErr: false,
},
{
name: "create run record error",
messages: []*workflow.EnterMessage{
{Role: "user", ContentType: "text", Content: "hello"},
},
setupMock: func() {
mockAgentRun.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil, errors.New("db error"))
},
expectErr: true,
},
{
name: "invalid role",
messages: []*workflow.EnterMessage{
{Role: "system", ContentType: "text", Content: "hello"},
},
setupMock: func() {},
expectErr: true,
},
{
name: "toConversationMessage error",
messages: []*workflow.EnterMessage{
{Role: "user", ContentType: "invalid", Content: "hello"},
},
setupMock: func() {
mockAgentRun.EXPECT().Create(gomock.Any(), gomock.Any()).Return(&agententity.RunRecordMeta{ID: 100}, nil)
},
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setupMock()
result, err := makeChatFlowHistoryMessages(ctx, bizID, conversationID, userID, sectionID, connectorID, tt.messages)
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}

View File

@ -108,5 +108,10 @@ func InitService(_ context.Context, components *ServiceComponents) (*Application
SVC.TosClient = components.Tos
SVC.IDGenerator = components.IDGen
err = SVC.InitNodeIconURLCache(context.Background())
if err != nil {
return nil, err
}
return SVC, nil
}

View File

@ -23,10 +23,12 @@ import (
"runtime/debug"
"strconv"
"strings"
"sync"
"time"
"github.com/cloudwego/eino/schema"
xmaps "golang.org/x/exp/maps"
"golang.org/x/sync/errgroup"
"github.com/coze-dev/coze-studio/backend/api/model/app/bot_common"
model "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/knowledge"
@ -75,12 +77,56 @@ type ApplicationService struct {
IDGenerator idgen.IDGenerator
}
var SVC = &ApplicationService{}
var (
SVC = &ApplicationService{}
nodeIconURLCache = make(map[string]string)
nodeIconURLCacheMu sync.Mutex
)
func GetWorkflowDomainSVC() domainWorkflow.Service {
return SVC.DomainSVC
}
func (w *ApplicationService) InitNodeIconURLCache(ctx context.Context) error {
category2NodeMetaList, _, err := GetWorkflowDomainSVC().ListNodeMeta(ctx, nil)
if err != nil {
logs.Errorf("failed to list node meta for icon url cache: %v", err)
return err
}
eg, gCtx := errgroup.WithContext(ctx)
for _, nodeMetaList := range category2NodeMetaList {
for _, nodeMeta := range nodeMetaList {
eg.Go(func() error {
if len(nodeMeta.IconURI) == 0 {
// For custom nodes, if IconURI is not set, there will be no icon.
logs.Warnf("node '%s' has an empty IconURI, it will have no icon", nodeMeta.Name)
return nil
}
url, err := w.TosClient.GetObjectUrl(gCtx, nodeMeta.IconURI)
if err != nil {
logs.Warnf("failed to get object url for node %s: %v", nodeMeta.Name, err)
return err
}
nodeTypeStr := entity.IDStrToNodeType(strconv.FormatInt(nodeMeta.ID, 10))
if len(nodeTypeStr) > 0 {
nodeIconURLCacheMu.Lock()
nodeIconURLCache[string(nodeTypeStr)] = url
nodeIconURLCacheMu.Unlock()
}
return nil
})
}
}
if err := eg.Wait(); err != nil {
return err
}
logs.Infof("node icon url cache initialized with %d entries", len(nodeIconURLCache))
return nil
}
func (w *ApplicationService) GetNodeTemplateList(ctx context.Context, req *workflow.NodeTemplateListRequest) (
_ *workflow.NodeTemplateListResponse, err error,
) {
@ -119,19 +165,22 @@ func (w *ApplicationService) GetNodeTemplateList(ctx context.Context, req *workf
Name: category,
}
for _, nodeMeta := range nodeMetaList {
nodeID := fmt.Sprintf("%d", nodeMeta.ID)
nodeType := entity.IDStrToNodeType(nodeID)
url := nodeIconURLCache[string(nodeType)]
tpl := &workflow.NodeTemplate{
ID: fmt.Sprintf("%d", nodeMeta.ID),
ID: nodeID,
Type: workflow.NodeTemplateType(nodeMeta.ID),
Name: ternary.IFElse(i18n.GetLocale(ctx) == i18n.LocaleEN, nodeMeta.EnUSName, nodeMeta.Name),
Desc: ternary.IFElse(i18n.GetLocale(ctx) == i18n.LocaleEN, nodeMeta.EnUSDescription, nodeMeta.Desc),
IconURL: nodeMeta.IconURL,
IconURL: url,
SupportBatch: ternary.IFElse(nodeMeta.SupportBatch, workflow.SupportBatch_SUPPORT, workflow.SupportBatch_NOT_SUPPORT),
NodeType: fmt.Sprintf("%d", nodeMeta.ID),
NodeType: nodeID,
Color: nodeMeta.Color,
}
resp.Data.TemplateList = append(resp.Data.TemplateList, tpl)
categoryMap[category].NodeTypeList = append(categoryMap[category].NodeTypeList, fmt.Sprintf("%d", nodeMeta.ID))
categoryMap[category].NodeTypeList = append(categoryMap[category].NodeTypeList, nodeID)
}
}
@ -750,11 +799,13 @@ func (w *ApplicationService) GetProcess(ctx context.Context, req *workflow.GetWo
}
}
iconURL := nodeIconURLCache[string(ie.NodeType)]
resp.Data.NodeEvents = append(resp.Data.NodeEvents, &workflow.NodeEvent{
ID: strconv.FormatInt(ie.ID, 10),
NodeID: string(ie.NodeKey),
NodeTitle: ie.NodeTitle,
NodeIcon: ie.NodeIcon,
NodeIcon: iconURL,
Data: ie.InterruptData,
Type: ie.EventType,
SchemaNodeID: string(ie.NodeKey),

View File

@ -59,7 +59,7 @@ type MessageListRequest struct {
BeforeID *string
AfterID *string
UserID int64
AppID int64
BizID int64
OrderBy *string
}
@ -89,7 +89,7 @@ type WfMessage struct {
type GetLatestRunIDsRequest struct {
ConversationID int64
UserID int64
AppID int64
BizID int64
Rounds int64
SectionID int64
InitRunID *int64

View File

@ -24,6 +24,7 @@ import (
var defaultSVC Uploader
//go:generate mockgen -destination uploadmock/upload_mock.go --package uploadmock -source upload.go
type Uploader interface {
GetFile(ctx context.Context, req *service.GetFileRequest) (resp *service.GetFileResponse, err error)
}

View File

@ -0,0 +1,73 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Code generated by MockGen. DO NOT EDIT.
// Source: upload.go
//
// Generated by this command:
//
// mockgen -destination uploadmock/upload_mock.go --package uploadmock -source upload.go
//
// Package uploadmock is a generated GoMock package.
package uploadmock
import (
context "context"
reflect "reflect"
service "github.com/coze-dev/coze-studio/backend/domain/upload/service"
gomock "go.uber.org/mock/gomock"
)
// MockUploader is a mock of Uploader interface.
type MockUploader struct {
ctrl *gomock.Controller
recorder *MockUploaderMockRecorder
isgomock struct{}
}
// MockUploaderMockRecorder is the mock recorder for MockUploader.
type MockUploaderMockRecorder struct {
mock *MockUploader
}
// NewMockUploader creates a new mock instance.
func NewMockUploader(ctrl *gomock.Controller) *MockUploader {
mock := &MockUploader{ctrl: ctrl}
mock.recorder = &MockUploaderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockUploader) EXPECT() *MockUploaderMockRecorder {
return m.recorder
}
// GetFile mocks base method.
func (m *MockUploader) GetFile(ctx context.Context, req *service.GetFileRequest) (*service.GetFileResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetFile", ctx, req)
ret0, _ := ret[0].(*service.GetFileResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetFile indicates an expected call of GetFile.
func (mr *MockUploaderMockRecorder) GetFile(ctx, req any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFile", reflect.TypeOf((*MockUploader)(nil).GetFile), ctx, req)
}

View File

@ -54,7 +54,7 @@ func (c *impl) MessageList(ctx context.Context, req *crossmessage.MessageListReq
ConversationID: req.ConversationID,
Limit: int(req.Limit), // Since the value of limit is checked inside the node, the type cast here is safe
UserID: strconv.FormatInt(req.UserID, 10),
AgentID: req.AppID,
AgentID: req.BizID,
OrderBy: req.OrderBy,
}
if req.BeforeID != nil {
@ -96,7 +96,7 @@ func (c *impl) MessageList(ctx context.Context, req *crossmessage.MessageListReq
func (c *impl) GetLatestRunIDs(ctx context.Context, req *crossmessage.GetLatestRunIDsRequest) ([]int64, error) {
listMeta := &agententity.ListRunRecordMeta{
ConversationID: req.ConversationID,
AgentID: req.AppID,
AgentID: req.BizID,
Limit: int32(req.Rounds),
SectionID: req.SectionID,
}

View File

@ -75,11 +75,11 @@ type Conversation interface {
ListDynamicConversation(ctx context.Context, env vo.Env, policy *vo.ListConversationPolicy) ([]*entity.DynamicConversation, error)
ReleaseConversationTemplate(ctx context.Context, appID int64, version string) error
InitApplicationDefaultConversationTemplate(ctx context.Context, spaceID int64, appID int64, userID int64) error
GetOrCreateConversation(ctx context.Context, env vo.Env, appID, connectorID, userID int64, conversationName string) (int64, int64, error)
GetOrCreateConversation(ctx context.Context, env vo.Env, bizID, connectorID, userID int64, conversationName string) (int64, int64, error)
UpdateConversation(ctx context.Context, env vo.Env, appID, connectorID, userID int64, conversationName string) (int64, error)
GetTemplateByName(ctx context.Context, env vo.Env, appID int64, templateName string) (*entity.ConversationTemplate, bool, error)
GetDynamicConversationByName(ctx context.Context, env vo.Env, appID, connectorID, userID int64, name string) (*entity.DynamicConversation, bool, error)
GetConversationNameByID(ctx context.Context, env vo.Env, appID, connectorID, conversationID int64) (string, bool, error)
GetConversationNameByID(ctx context.Context, env vo.Env, bizID, connectorID, conversationID int64) (string, bool, error)
}
type InterruptEventStore interface {
@ -143,8 +143,8 @@ type ConversationRepository interface {
UpdateStaticConversation(ctx context.Context, env vo.Env, templateID int64, connectorID int64, userID int64, newConversationID int64) error
UpdateDynamicConversation(ctx context.Context, env vo.Env, conversationID, newConversationID int64) error
CopyTemplateConversationByAppID(ctx context.Context, appID int64, toAppID int64) error
GetStaticConversationByID(ctx context.Context, env vo.Env, appID, connectorID, conversationID int64) (string, bool, error)
GetDynamicConversationByID(ctx context.Context, env vo.Env, appID, connectorID, conversationID int64) (*entity.DynamicConversation, bool, error)
GetStaticConversationByID(ctx context.Context, env vo.Env, bizID, connectorID, conversationID int64) (string, bool, error)
GetDynamicConversationByID(ctx context.Context, env vo.Env, bizID, connectorID, conversationID int64) (*entity.DynamicConversation, bool, error)
}
type WorkflowConfig interface {
GetNodeOfCodeConfig() *config.NodeOfCodeConfig

View File

@ -53,7 +53,7 @@ type NodeTypeMeta struct {
Category string `json:"category"`
Color string `json:"color"`
Desc string `json:"desc"`
IconURL string `json:"icon_url"`
IconURI string `json:"icon_uri"`
SupportBatch bool `json:"support_batch"`
Disabled bool `json:"disabled,omitempty"`
EnUSName string `json:"en_us_name,omitempty"`
@ -265,7 +265,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "input&output",
Desc: "工作流的起始节点,用于设定启动工作流需要的信息",
Color: "#5C62FF",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Start-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-start.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PostFillNil: true,
@ -281,7 +281,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "input&output",
Desc: "工作流的最终节点,用于返回工作流运行后的结果信息",
Color: "#5C62FF",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-End-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-end.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -299,7 +299,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "",
Desc: "调用大语言模型,使用变量和提示词生成回复",
Color: "#5C62FF",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-llm.jpg",
SupportBatch: true,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -319,7 +319,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "",
Desc: "通过添加工具访问实时数据和执行外部操作",
Color: "#CA61FF",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Plugin-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-plugin.jpg",
SupportBatch: true,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -337,7 +337,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "logic",
Desc: "编写代码,处理输入变量来生成返回值",
Color: "#00B2B2",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Code-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-code.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -355,7 +355,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "data",
Desc: "在选定的知识中,根据输入变量召回最匹配的信息,并以列表形式返回",
Color: "#FF811A",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-KnowledgeQuery-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-knowledge-query.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -374,7 +374,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "logic",
Desc: "连接多个下游分支,若设定的条件成立则仅运行对应的分支,若均不成立则只运行“否则”分支",
Color: "#00B2B2",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Condition-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-condition.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{},
EnUSName: "Condition",
@ -388,7 +388,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "",
Desc: "集成已发布工作流,可以执行嵌套子任务",
Color: "#00B83E",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Workflow-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-workflow.jpg",
SupportBatch: true,
ExecutableMeta: ExecutableMeta{
BlockEndStream: true,
@ -404,7 +404,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "database",
Desc: "基于用户自定义的 SQL 完成对数据库的增删改查操作",
Color: "#FF811A",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Database-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-database.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -422,7 +422,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "input&output",
Desc: "节点从“消息”更名为“输出”,支持中间过程的消息输出,支持流式和非流式两种方式",
Color: "#5C62FF",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Output-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-output.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -441,7 +441,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "utilities",
Desc: "用于处理多个字符串类型变量的格式",
Color: "#3071F2",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-StrConcat-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-text.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -458,7 +458,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "utilities",
Desc: "支持中间向用户提问问题,支持预置选项提问和开放式问题提问两种方式",
Color: "#3071F2",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Direct-Question-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-question.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -477,7 +477,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "logic",
Desc: "用于立即终止当前所在的循环,跳出循环体",
Color: "#00B2B2",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Break-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-break.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{},
EnUSName: "Break",
@ -491,7 +491,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "logic",
Desc: "用于重置循环变量的值,使其下次循环使用重置后的值",
Color: "#00B2B2",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LoopSetVariable-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-loop-set-variable.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{},
EnUSName: "Set Variable",
@ -505,7 +505,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "logic",
Desc: "用于通过设定循环次数和逻辑,重复执行一系列任务",
Color: "#00B2B2",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Loop-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-loop.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
IsComposite: true,
@ -524,7 +524,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "logic",
Desc: "用于用户输入的意图识别,并将其与预设意图选项进行匹配。",
Color: "#00B2B2",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Intent-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-intent.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -543,7 +543,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "data",
Desc: "写入节点可以添加 文本类型 的知识库,仅可以添加一个知识库",
Color: "#FF811A",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-KnowledgeWriting-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-knowledge-write.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -561,7 +561,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "logic",
Desc: "通过设定批量运行次数和逻辑,运行批处理体内的任务",
Color: "#00B2B2",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Batch-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-batch.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
IsComposite: true,
@ -580,7 +580,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "logic",
Desc: "用于终止当前循环,执行下次循环",
Color: "#00B2B2",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Continue-v2.jpg",
IconURI: "default_icon/workflow_icon/icon-continue.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{},
EnUSName: "Continue",
@ -594,7 +594,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "input&output",
Desc: "支持中间过程的信息输入",
Color: "#5C62FF",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Input-v2.jpg",
IconURI: "default_icon/workflow_icon/icon_input.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PostFillNil: true,
@ -609,8 +609,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "", // Not found in cate_list
Desc: "comment_desc", // Placeholder from JSON
Color: "",
IconURL: "comment_icon", // Placeholder from JSON
SupportBatch: false, // supportBatch: 1
SupportBatch: false, // supportBatch: 1
EnUSName: "Comment",
},
NodeTypeVariableAggregator: {
@ -620,7 +619,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "logic",
Desc: "对多个分支的输出进行聚合处理",
Color: "#00B2B2",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/VariableMerge-icon.jpg",
IconURI: "default_icon/workflow_icon/icon-variable-merge.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PostFillNil: true,
@ -638,7 +637,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "message",
Desc: "用于查询消息列表",
Color: "#F2B600",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Conversation-List.jpeg",
IconURI: "default_icon/workflow_icon/icon-query-message-list.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -654,7 +653,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "conversation_history", // Mapped from cate_list
Desc: "用于清空会话历史清空后LLM看到的会话历史为空",
Color: "#F2B600",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Conversation-Delete.jpeg",
IconURI: "default_icon/workflow_icon/icon-clear-context.jpg",
SupportBatch: false, // supportBatch: 1
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -670,7 +669,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "conversation_management",
Desc: "用于创建会话",
Color: "#F2B600",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Conversation-Create.jpeg",
IconURI: "default_icon/workflow_icon/icon-create-conversation.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -687,7 +686,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "data",
Desc: "用于给支持写入的变量赋值,包括应用变量、用户变量",
Color: "#FF811A",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/Variable.jpg",
IconURI: "default_icon/workflow_icon/icon-variable-assign.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{},
EnUSName: "Variable assign",
@ -701,7 +700,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "database",
Desc: "修改表中已存在的数据记录,用户指定更新条件和内容来更新数据",
Color: "#F2B600",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-database-update.jpg",
IconURI: "default_icon/workflow_icon/icon-database-update.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -718,7 +717,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "database",
Desc: "从表获取数据,用户可定义查询条件、选择列等,输出符合条件的数据",
Color: "#F2B600",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icaon-database-select.jpg",
IconURI: "default_icon/workflow_icon/icon-database-query.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -735,7 +734,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "database",
Desc: "从表中删除数据记录,用户指定删除条件来删除符合条件的记录",
Color: "#F2B600",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-database-delete.jpg",
IconURI: "default_icon/workflow_icon/icon-database-delete.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -752,7 +751,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "utilities",
Desc: "用于发送API请求从接口返回数据",
Color: "#3071F2",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-HTTP.png",
IconURI: "default_icon/workflow_icon/icon-http.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -769,7 +768,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "database",
Desc: "向表添加新数据记录,用户输入数据内容后插入数据库",
Color: "#F2B600",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-database-insert.jpg",
IconURI: "default_icon/workflow_icon/icon-database-create.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -785,7 +784,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "conversation_management",
Desc: "用于修改会话的名字",
Color: "#F2B600",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-编辑会话.jpg",
IconURI: "default_icon/workflow_icon/icon-update-conversation.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -802,7 +801,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "conversation_management",
Desc: "用于删除会话",
Color: "#F2B600",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-删除会话.jpg",
IconURI: "default_icon/workflow_icon/icon-delete-conversation.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -818,7 +817,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "conversation_management",
Desc: "用于查询所有会话,包含静态会话、动态会话",
Color: "#F2B600",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-查询会话.jpg",
IconURI: "default_icon/workflow_icon/icon-query-conversation-list.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PostFillNil: true,
@ -833,7 +832,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "conversation_history", // Mapped from cate_list
Desc: "用于查询会话历史返回LLM可见的会话消息",
Color: "#F2B600",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-查询会话历史.jpg",
IconURI: "default_icon/workflow_icon/icon-query-conversation-history.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -849,7 +848,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "message",
Desc: "用于创建消息",
Color: "#F2B600",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-创建消息.jpg",
IconURI: "default_icon/workflow_icon/icon-create-message.jpg",
SupportBatch: false, // supportBatch: 1
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -865,7 +864,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "message",
Desc: "用于修改消息",
Color: "#F2B600",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-修改消息.jpg",
IconURI: "default_icon/workflow_icon/icon-update-message.jpg",
SupportBatch: false, // supportBatch: 1
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -881,7 +880,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "message",
Desc: "用于删除消息",
Color: "#F2B600",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-删除消息.jpg",
IconURI: "default_icon/workflow_icon/icon-delete-message.jpg",
SupportBatch: false, // supportBatch: 1
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -916,8 +915,8 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
// Color is the color of the upper edge of the node displayed on Canvas.
Color: "F2B600",
// IconURL is the URL of the icon displayed on Canvas.
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-to_json.png",
// IconURI is the resource identifier for the icon displayed on the Canvas. It's resolved into a full URL by the backend to support different deployment environments.
IconURI: "default_icon/workflow_icon/icon-json-stringify.jpg",
// SupportBatch indicates whether this node can set batch mode.
// NOTE: ultimately it's frontend that decides which node can enable batch mode.
@ -943,7 +942,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "utilities",
Desc: "用于将JSON字符串解析为变量",
Color: "F2B600",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-from_json.png",
IconURI: "default_icon/workflow_icon/icon-json-parser.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,
@ -961,7 +960,7 @@ var NodeTypeMetas = map[NodeType]*NodeTypeMeta{
Category: "data",
Desc: "用于删除知识库中的文档",
Color: "#FF811A",
IconURL: "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icons-dataset-delete.png",
IconURI: "default_icon/workflow_icon/icon-knowledge-delete.jpg",
SupportBatch: false,
ExecutableMeta: ExecutableMeta{
PreFillZero: true,

View File

@ -32,6 +32,11 @@ const (
ChatFlowMessageCompleted ChatFlowEvent = "conversation.message.completed"
)
const (
ConversationNameKey = "CONVERSATION_NAME"
UserInputKey = "USER_INPUT"
)
type Usage struct {
TokenCount *int32 `form:"token_count" json:"token_count,omitempty"`
OutputTokens *int32 `form:"output_count" json:"output_count,omitempty"`

View File

@ -59,14 +59,14 @@ type ListConversationPolicy struct {
}
type CreateStaticConversation struct {
AppID int64
BizID int64
UserID int64
ConnectorID int64
TemplateID int64
}
type CreateDynamicConversation struct {
AppID int64
BizID int64
UserID int64
ConnectorID int64

View File

@ -44,8 +44,14 @@ type Reference struct {
}
type FieldSource struct {
Ref *Reference `json:"ref,omitempty"`
Val any `json:"val,omitempty"`
Ref *Reference `json:"ref,omitempty"`
Val any `json:"val,omitempty"`
FileExtra *FileExtra `json:"file_extra,omitempty"`
}
type FileExtra struct {
FileName *string `json:"file_name,omitempty"`
FileNames []string `json:"file_names,omitempty"`
}
type TypeInfo struct {

View File

@ -731,6 +731,20 @@ func TestKnowledgeDeleter(t *testing.T) {
UserID: 123,
})
defer mockey.Mock(execute.GetExeCtx).Return(&execute.Context{
RootCtx: execute.RootCtx{
ExeCfg: workflowModel.ExecuteConfig{
InputFileFields: map[string]*workflowModel.FileInfo{
"https://p26-bot-workflow-sign.byteimg.com/tos-cn-i-mdko3gqilj/5264fa1295da4a6483cd236b1316c454.pdf~tplv-mdko3gqilj-image.image?rk3s=81d4c505&x-expires=1782379180&x-signature=mlaXPIk9VJjOXu87xGaRmNRg9%2BA%3D": &workflowModel.FileInfo{
FileName: "1706.03762v7.pdf",
FileURL: "https://p26-bot-workflow-sign.byteimg.com/tos-cn-i-mdko3gqilj/5264fa1295da4a6483cd236b1316c454.pdf~tplv-mdko3gqilj-image.image?rk3s=81d4c505&x-expires=1782379180&x-signature=mlaXPIk9VJjOXu87xGaRmNRg9%2BA%3D",
FileExtension: ".pdf",
},
},
},
},
}).Build().UnPatch()
workflowSC, err := CanvasToWorkflowSchema(ctx, c)
assert.NoError(t, err)

View File

@ -235,6 +235,7 @@ func WorkflowSchemaFromNode(ctx context.Context, c *vo.Canvas, nodeID string) (
if enabled {
trimmedSC.GeneratedNodes = append(trimmedSC.GeneratedNodes, ns.Key)
}
trimmedSC.Init()
return trimmedSC, nil
}

View File

@ -18,6 +18,7 @@ package convert
import (
"fmt"
"strconv"
"strings"
@ -25,6 +26,7 @@ import (
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/schema"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
"github.com/coze-dev/coze-studio/backend/pkg/sonic"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
@ -180,7 +182,10 @@ func CanvasBlockInputToFieldInfo(b *vo.BlockInput, path einoCompose.FieldPath, p
if value == nil {
return nil, fmt.Errorf("input %v has no value, type= %s", path, b.Type)
}
var fileExtra *vo.FileExtra
isFileAssistType := func(assistType vo.AssistType) bool {
return assistType >= vo.AssistTypeDefault && assistType <= vo.AssistTypeVoice
}
switch value.Type {
case vo.BlockInputValueTypeObjectRef:
sc := b.Schema
@ -214,7 +219,6 @@ func CanvasBlockInputToFieldInfo(b *vo.BlockInput, path einoCompose.FieldPath, p
if content == nil {
return nil, fmt.Errorf("input %v is literal but has no value, type= %s", path, b.Type)
}
switch b.Type {
case vo.VariableTypeObject:
m := make(map[string]any)
@ -223,11 +227,43 @@ func CanvasBlockInputToFieldInfo(b *vo.BlockInput, path einoCompose.FieldPath, p
}
content = m
case vo.VariableTypeList:
l := make([]any, 0)
if err = sonic.UnmarshalString(content.(string), &l); err != nil {
return nil, err
switch content.(type) {
case string:
if _, ok := content.(string); ok {
l := make([]any, 0)
if err = sonic.UnmarshalString(content.(string), &l); err != nil {
return nil, err
}
content = l
}
case []string:
content = content.([]string)
case []any:
content = content.([]any)
default:
return nil, fmt.Errorf("unsupported variable type fot list: %s", b.Type)
}
eleSchema, err := vo.ParseVariable(b.Schema)
if err != nil {
return nil, fmt.Errorf("can not parse schema from %v", b.Schema)
}
if isFileAssistType(eleSchema.AssistType) {
rawMeta, ok := b.Value.RawMeta.(map[string]any)
if ok {
filenames, ok := rawMeta["fileName"].([]any)
if !ok {
return nil, fmt.Errorf("can not get filename from %v", rawMeta)
}
fileExtra = &vo.FileExtra{
FileNames: make([]string, 0, len(filenames)),
}
for _, filename := range filenames {
fileExtra.FileNames = append(fileExtra.FileNames, filename.(string))
}
}
}
content = l
case vo.VariableTypeInteger:
switch content.(type) {
case string:
@ -268,13 +304,27 @@ func CanvasBlockInputToFieldInfo(b *vo.BlockInput, path einoCompose.FieldPath, p
default:
return nil, fmt.Errorf("unsupported variable type for boolean: %s", b.Type)
}
case vo.VariableTypeString:
if isFileAssistType(b.AssistType) {
rawMeta, ok := b.Value.RawMeta.(map[string]any)
if ok {
filename, ok := rawMeta["fileName"].(string)
if !ok {
return nil, fmt.Errorf("can not get filename from %v", rawMeta)
}
fileExtra = &vo.FileExtra{
FileName: ptr.Of(filename),
}
}
}
default:
}
return []*vo.FieldInfo{
{
Path: path,
Source: vo.FieldSource{
Val: content,
Val: content,
FileExtra: fileExtra,
},
},
}, nil
@ -466,8 +516,8 @@ func SetInputsForNodeSchema(n *vo.Node, ns *schema.NodeSchema) error {
if err != nil {
return err
}
ns.AddInputSource(sources...)
}
return nil

View File

@ -0,0 +1,275 @@
{
"nodes": [
{
"id": "100001",
"type": "1",
"meta": {
"position": {
"x": 180,
"y": 79.2
}
},
"data": {
"outputs": [
{
"type": "string",
"name": "USER_INPUT",
"required": false
},
{
"type": "string",
"name": "CONVERSATION_NAME",
"required": false,
"description": "本次请求绑定的会话,会自动写入消息、会从该会话读对话历史。",
"defaultValue": "dhl"
}
],
"nodeMeta": {
"title": "开始",
"icon": "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Start-v2.jpg",
"description": "工作流的起始节点,用于设定启动工作流需要的信息",
"subTitle": ""
},
"trigger_parameters": []
}
},
{
"id": "900001",
"type": "2",
"meta": {
"position": {
"x": 2020,
"y": 66.2
}
},
"data": {
"nodeMeta": {
"title": "结束",
"icon": "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-End-v2.jpg",
"description": "工作流的最终节点,用于返回工作流运行后的结果信息",
"subTitle": ""
},
"inputs": {
"terminatePlan": "useAnswerContent",
"streamingOutput": true,
"inputParameters": [
{
"name": "output",
"input": {
"type": "string",
"value": {
"type": "ref",
"content": {
"source": "block-output",
"blockID": "142077",
"name": "optionContent"
},
"rawMeta": {
"type": 1
}
}
}
}
],
"content": {
"type": "string",
"value": {
"type": "literal",
"content": "{{output}}"
}
}
}
}
},
{
"id": "190196",
"type": "30",
"meta": {
"position": {
"x": 640,
"y": 78.5
}
},
"data": {
"outputs": [
{
"type": "string",
"name": "input",
"required": true
}
],
"nodeMeta": {
"title": "输入",
"icon": "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Input-v2.jpg",
"description": "支持中间过程的信息输入",
"mainColor": "#5C62FF",
"subTitle": "输入"
},
"inputs": {
"outputSchema": "[{\"type\":\"string\",\"name\":\"input\",\"required\":true}]"
}
}
},
{
"id": "133775",
"type": "18",
"meta": {
"position": {
"x": 1100,
"y": 39
}
},
"data": {
"inputs": {
"llmParam": {
"modelType": 1001,
"modelName": "Doubao-Seed-1.6",
"generationDiversity": "balance",
"temperature": 0.8,
"maxTokens": 4096,
"topP": 0.7,
"responseFormat": 2,
"systemPrompt": ""
},
"inputParameters": [],
"extra_output": false,
"answer_type": "text",
"option_type": "static",
"dynamic_option": {
"type": "string",
"value": {
"type": "ref",
"content": {
"source": "block-output",
"blockID": "",
"name": ""
}
}
},
"question": "你好",
"options": [
{
"name": ""
},
{
"name": ""
}
],
"limit": 3
},
"nodeMeta": {
"title": "问答",
"icon": "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Direct-Question-v2.jpg",
"description": "支持中间向用户提问问题,支持预置选项提问和开放式问题提问两种方式",
"mainColor": "#3071F2",
"subTitle": "问答"
},
"outputs": [
{
"type": "string",
"name": "USER_RESPONSE",
"required": true,
"description": "用户本轮对话输入内容"
}
]
}
},
{
"id": "142077",
"type": "18",
"meta": {
"position": {
"x": 1560,
"y": 0
}
},
"data": {
"inputs": {
"llmParam": {
"modelType": 1001,
"modelName": "Doubao-Seed-1.6",
"generationDiversity": "balance",
"temperature": 0.8,
"maxTokens": 4096,
"topP": 0.7,
"responseFormat": 2,
"systemPrompt": ""
},
"inputParameters": [],
"extra_output": false,
"answer_type": "option",
"option_type": "static",
"dynamic_option": {
"type": "string",
"value": {
"type": "ref",
"content": {
"source": "block-output",
"blockID": "",
"name": ""
}
}
},
"question": "请选择",
"options": [
{
"name": "A"
},
{
"name": "B"
}
],
"limit": 3
},
"nodeMeta": {
"title": "问答_1",
"icon": "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Direct-Question-v2.jpg",
"description": "支持中间向用户提问问题,支持预置选项提问和开放式问题提问两种方式",
"mainColor": "#3071F2",
"subTitle": "问答"
},
"outputs": [
{
"type": "string",
"name": "optionId",
"required": false
},
{
"type": "string",
"name": "optionContent",
"required": false
}
]
}
}
],
"edges": [
{
"sourceNodeID": "100001",
"targetNodeID": "190196"
},
{
"sourceNodeID": "142077",
"targetNodeID": "900001",
"sourcePortID": "branch_0"
},
{
"sourceNodeID": "142077",
"targetNodeID": "900001",
"sourcePortID": "branch_1"
},
{
"sourceNodeID": "142077",
"targetNodeID": "900001",
"sourcePortID": "default"
},
{
"sourceNodeID": "190196",
"targetNodeID": "133775"
},
{
"sourceNodeID": "133775",
"targetNodeID": "142077"
}
]
}

View File

@ -0,0 +1,397 @@
{
"nodes": [
{
"blocks": [],
"data": {
"nodeMeta": {
"description": "工作流的起始节点,用于设定启动工作流需要的信息",
"icon": "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Start-v2.jpg",
"subTitle": "",
"title": "开始"
},
"outputs": [
{
"name": "USER_INPUT",
"required": false,
"type": "string"
},
{
"defaultValue": "Default",
"description": "本次请求绑定的会话,会自动写入消息、会从该会话读对话历史。",
"name": "CONVERSATION_NAME",
"required": false,
"type": "string"
}
],
"trigger_parameters": []
},
"edges": null,
"id": "100001",
"meta": {
"position": {
"x": 0,
"y": 0
}
},
"type": "1"
},
{
"blocks": [],
"data": {
"inputs": {
"content": {
"type": "string",
"value": {
"content": "{{output}}",
"type": "literal"
}
},
"inputParameters": [
{
"input": {
"type": "string",
"value": {
"content": {
"blockID": "123887",
"name": "output",
"source": "block-output"
},
"rawMeta": {
"type": 1
},
"type": "ref"
}
},
"name": "output"
}
],
"streamingOutput": true,
"terminatePlan": "useAnswerContent"
},
"nodeMeta": {
"description": "工作流的最终节点,用于返回工作流运行后的结果信息",
"icon": "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-End-v2.jpg",
"subTitle": "",
"title": "结束"
}
},
"edges": null,
"id": "900001",
"meta": {
"position": {
"x": 926,
"y": -13
}
},
"type": "2"
},
{
"blocks": [],
"data": {
"inputs": {
"fcParamVar": {
"knowledgeFCParam": {}
},
"inputParameters": [
{
"input": {
"type": "string",
"value": {
"content": {
"blockID": "100001",
"name": "USER_INPUT",
"source": "block-output"
},
"rawMeta": {
"type": 1
},
"type": "ref"
}
},
"name": "input"
}
],
"llmParam": [
{
"input": {
"type": "integer",
"value": {
"content": "1737521813",
"rawMeta": {
"type": 2
},
"type": "literal"
}
},
"name": "modelType"
},
{
"input": {
"type": "string",
"value": {
"content": "豆包·1.5·Pro·32k",
"rawMeta": {
"type": 1
},
"type": "literal"
}
},
"name": "modleName"
},
{
"input": {
"type": "string",
"value": {
"content": "balance",
"rawMeta": {
"type": 1
},
"type": "literal"
}
},
"name": "generationDiversity"
},
{
"input": {
"type": "float",
"value": {
"content": "0.8",
"rawMeta": {
"type": 4
},
"type": "literal"
}
},
"name": "temperature"
},
{
"input": {
"type": "integer",
"value": {
"content": "4096",
"rawMeta": {
"type": 2
},
"type": "literal"
}
},
"name": "maxTokens"
},
{
"input": {
"type": "boolean",
"value": {
"content": false,
"rawMeta": {
"type": 3
},
"type": "literal"
}
},
"name": "spCurrentTime"
},
{
"input": {
"type": "boolean",
"value": {
"content": false,
"rawMeta": {
"type": 3
},
"type": "literal"
}
},
"name": "spAntiLeak"
},
{
"input": {
"type": "boolean",
"value": {
"content": false,
"rawMeta": {
"type": 3
},
"type": "literal"
}
},
"name": "prefixCache"
},
{
"input": {
"type": "integer",
"value": {
"content": "2",
"rawMeta": {
"type": 2
},
"type": "literal"
}
},
"name": "responseFormat"
},
{
"input": {
"type": "string",
"value": {
"content": "{{input}}",
"rawMeta": {
"type": 1
},
"type": "literal"
}
},
"name": "prompt"
},
{
"input": {
"type": "boolean",
"value": {
"content": false,
"rawMeta": {
"type": 3
},
"type": "literal"
}
},
"name": "enableChatHistory"
},
{
"input": {
"type": "integer",
"value": {
"content": "3",
"rawMeta": {
"type": 2
},
"type": "literal"
}
},
"name": "chatHistoryRound"
},
{
"input": {
"type": "string",
"value": {
"content": "",
"rawMeta": {
"type": 1
},
"type": "literal"
}
},
"name": "systemPrompt"
},
{
"input": {
"type": "string",
"value": {
"content": "",
"rawMeta": {
"type": 1
},
"type": "literal"
}
},
"name": "stableSystemPrompt"
},
{
"input": {
"type": "boolean",
"value": {
"content": false,
"rawMeta": {
"type": 3
},
"type": "literal"
}
},
"name": "canContinue"
},
{
"input": {
"type": "string",
"value": {
"content": "",
"rawMeta": {
"type": 1
},
"type": "literal"
}
},
"name": "loopPromptVersion"
},
{
"input": {
"type": "string",
"value": {
"content": "",
"rawMeta": {
"type": 1
},
"type": "literal"
}
},
"name": "loopPromptName"
},
{
"input": {
"type": "string",
"value": {
"content": "",
"rawMeta": {
"type": 1
},
"type": "literal"
}
},
"name": "loopPromptId"
}
],
"settingOnError": {
"processType": 1,
"retryTimes": 0,
"timeoutMs": 180000
}
},
"nodeMeta": {
"description": "调用大语言模型,使用变量和提示词生成回复",
"icon": "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg",
"mainColor": "#5C62FF",
"subTitle": "大模型",
"title": "大模型"
},
"outputs": [
{
"name": "output",
"type": "string"
}
],
"version": "3"
},
"edges": null,
"id": "123887",
"meta": {
"position": {
"x": 463,
"y": -39
}
},
"type": "3"
}
],
"edges": [
{
"sourceNodeID": "100001",
"targetNodeID": "123887",
"sourcePortID": ""
},
{
"sourceNodeID": "123887",
"targetNodeID": "900001",
"sourcePortID": ""
}
],
"versions": {
"loop": "v2"
}
}

View File

@ -0,0 +1,397 @@
{
"nodes": [
{
"blocks": [],
"data": {
"nodeMeta": {
"description": "工作流的起始节点,用于设定启动工作流需要的信息",
"icon": "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Start-v2.jpg",
"subTitle": "",
"title": "开始"
},
"outputs": [
{
"name": "USER_INPUT",
"required": false,
"type": "string"
},
{
"defaultValue": "Default",
"description": "本次请求绑定的会话,会自动写入消息、会从该会话读对话历史。",
"name": "CONVERSATION_NAME",
"required": false,
"type": "string"
}
],
"trigger_parameters": []
},
"edges": null,
"id": "100001",
"meta": {
"position": {
"x": 0,
"y": 0
}
},
"type": "1"
},
{
"blocks": [],
"data": {
"inputs": {
"content": {
"type": "string",
"value": {
"content": "{{output}}",
"type": "literal"
}
},
"inputParameters": [
{
"input": {
"type": "string",
"value": {
"content": {
"blockID": "123887",
"name": "output",
"source": "block-output"
},
"rawMeta": {
"type": 1
},
"type": "ref"
}
},
"name": "output"
}
],
"streamingOutput": true,
"terminatePlan": "useAnswerContent"
},
"nodeMeta": {
"description": "工作流的最终节点,用于返回工作流运行后的结果信息",
"icon": "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-End-v2.jpg",
"subTitle": "",
"title": "结束"
}
},
"edges": null,
"id": "900001",
"meta": {
"position": {
"x": 926,
"y": -13
}
},
"type": "2"
},
{
"blocks": [],
"data": {
"inputs": {
"fcParamVar": {
"knowledgeFCParam": {}
},
"inputParameters": [
{
"input": {
"type": "string",
"value": {
"content": {
"blockID": "100001",
"name": "USER_INPUT",
"source": "block-output"
},
"rawMeta": {
"type": 1
},
"type": "ref"
}
},
"name": "input"
}
],
"llmParam": [
{
"input": {
"type": "integer",
"value": {
"content": "1737521813",
"rawMeta": {
"type": 2
},
"type": "literal"
}
},
"name": "modelType"
},
{
"input": {
"type": "string",
"value": {
"content": "豆包·1.5·Pro·32k",
"rawMeta": {
"type": 1
},
"type": "literal"
}
},
"name": "modleName"
},
{
"input": {
"type": "string",
"value": {
"content": "balance",
"rawMeta": {
"type": 1
},
"type": "literal"
}
},
"name": "generationDiversity"
},
{
"input": {
"type": "float",
"value": {
"content": "0.8",
"rawMeta": {
"type": 4
},
"type": "literal"
}
},
"name": "temperature"
},
{
"input": {
"type": "integer",
"value": {
"content": "4096",
"rawMeta": {
"type": 2
},
"type": "literal"
}
},
"name": "maxTokens"
},
{
"input": {
"type": "boolean",
"value": {
"content": false,
"rawMeta": {
"type": 3
},
"type": "literal"
}
},
"name": "spCurrentTime"
},
{
"input": {
"type": "boolean",
"value": {
"content": false,
"rawMeta": {
"type": 3
},
"type": "literal"
}
},
"name": "spAntiLeak"
},
{
"input": {
"type": "boolean",
"value": {
"content": false,
"rawMeta": {
"type": 3
},
"type": "literal"
}
},
"name": "prefixCache"
},
{
"input": {
"type": "integer",
"value": {
"content": "2",
"rawMeta": {
"type": 2
},
"type": "literal"
}
},
"name": "responseFormat"
},
{
"input": {
"type": "string",
"value": {
"content": "{{input}}",
"rawMeta": {
"type": 1
},
"type": "literal"
}
},
"name": "prompt"
},
{
"input": {
"type": "boolean",
"value": {
"content": true,
"rawMeta": {
"type": 3
},
"type": "literal"
}
},
"name": "enableChatHistory"
},
{
"input": {
"type": "integer",
"value": {
"content": "3",
"rawMeta": {
"type": 2
},
"type": "literal"
}
},
"name": "chatHistoryRound"
},
{
"input": {
"type": "string",
"value": {
"content": "",
"rawMeta": {
"type": 1
},
"type": "literal"
}
},
"name": "systemPrompt"
},
{
"input": {
"type": "string",
"value": {
"content": "",
"rawMeta": {
"type": 1
},
"type": "literal"
}
},
"name": "stableSystemPrompt"
},
{
"input": {
"type": "boolean",
"value": {
"content": false,
"rawMeta": {
"type": 3
},
"type": "literal"
}
},
"name": "canContinue"
},
{
"input": {
"type": "string",
"value": {
"content": "",
"rawMeta": {
"type": 1
},
"type": "literal"
}
},
"name": "loopPromptVersion"
},
{
"input": {
"type": "string",
"value": {
"content": "",
"rawMeta": {
"type": 1
},
"type": "literal"
}
},
"name": "loopPromptName"
},
{
"input": {
"type": "string",
"value": {
"content": "",
"rawMeta": {
"type": 1
},
"type": "literal"
}
},
"name": "loopPromptId"
}
],
"settingOnError": {
"processType": 1,
"retryTimes": 0,
"timeoutMs": 180000
}
},
"nodeMeta": {
"description": "调用大语言模型,使用变量和提示词生成回复",
"icon": "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-LLM-v2.jpg",
"mainColor": "#5C62FF",
"subTitle": "大模型",
"title": "大模型"
},
"outputs": [
{
"name": "output",
"type": "string"
}
],
"version": "3"
},
"edges": null,
"id": "123887",
"meta": {
"position": {
"x": 463,
"y": -39
}
},
"type": "3"
}
],
"edges": [
{
"sourceNodeID": "100001",
"targetNodeID": "123887",
"sourcePortID": ""
},
{
"sourceNodeID": "123887",
"targetNodeID": "900001",
"sourcePortID": ""
}
],
"versions": {
"loop": "v2"
}
}

View File

@ -0,0 +1,344 @@
{
"nodes": [
{
"id": "100001",
"type": "1",
"meta": {
"position": {
"x": 180,
"y": 26.700000000000003
}
},
"data": {
"nodeMeta": {
"description": "工作流的起始节点,用于设定启动工作流需要的信息",
"icon": "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Start-v2.jpg",
"subTitle": "",
"title": "开始"
},
"outputs": [
{
"type": "string",
"assistType": 6,
"name": "f",
"required": false
},
{
"type": "list",
"name": "fs",
"schema": {
"type": "string",
"assistType": 2
},
"required": true
}
],
"trigger_parameters": []
}
},
{
"id": "900001",
"type": "2",
"meta": {
"position": {
"x": 2020,
"y": 13.700000000000003
}
},
"data": {
"nodeMeta": {
"description": "工作流的最终节点,用于返回工作流运行后的结果信息",
"icon": "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-End-v2.jpg",
"subTitle": "",
"title": "结束"
},
"inputs": {
"terminatePlan": "returnVariables",
"inputParameters": [
{
"name": "output",
"input": {
"type": "list",
"schema": {
"type": "string",
"assistType": 2
},
"value": {
"type": "ref",
"content": {
"source": "block-output",
"blockID": "100001",
"name": "fs"
},
"rawMeta": {
"type": 104
}
}
}
},
{
"name": "filename",
"input": {
"type": "string",
"value": {
"type": "ref",
"content": {
"source": "block-output",
"blockID": "129362",
"name": "fileName"
},
"rawMeta": {
"type": 1
}
}
}
}
]
}
}
},
{
"id": "191034",
"type": "5",
"meta": {
"position": {
"x": 1532.6813186813188,
"y": -19.285714285714285
}
},
"data": {
"nodeMeta": {
"title": "代码",
"icon": "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-Code-v2.jpg",
"description": "编写代码,处理输入变量来生成返回值",
"mainColor": "#00B2B2",
"subTitle": "代码"
},
"inputs": {
"inputParameters": [
{
"name": "input",
"input": {
"type": "string",
"value": {
"type": "ref",
"content": {
"source": "block-output",
"blockID": "129362",
"name": "fileName"
},
"rawMeta": {
"type": 1
}
}
}
}
],
"code": "# 在这里,您可以通过 'args' 获取节点中的输入变量,并通过 'ret' 输出结果\n# 'args' 已经被正确地注入到环境中\n# 下面是一个示例首先获取节点的全部输入参数params其次获取其中参数名为'input'的值:\n# params = args.params; \n# input = params['input'];\n# 下面是一个示例,输出一个包含多种数据类型的 'ret' 对象:\n# ret: Output = { \"name\": '小明', \"hobbies\": [\"看书\", \"旅游\"] };\n\nasync def main(args: Args) -> Output:\n params = args.params\n # 构建输出对象\n ret: Output = {\n \"key0\": params['input'] + params['input'], # 拼接两次入参 input 的值\n \"key1\": [\"hello\", \"world\"], # 输出一个数组\n \"key2\": { # 输出一个Object \n \"key21\": \"hi\"\n },\n }\n return ret",
"language": 3,
"settingOnError": {
"processType": 1,
"timeoutMs": 60000,
"retryTimes": 0
}
},
"outputs": [
{
"type": "string",
"name": "key0"
},
{
"type": "list",
"name": "key1",
"schema": {
"type": "string"
}
},
{
"type": "object",
"name": "key2",
"schema": [
{
"type": "string",
"name": "key21"
}
]
}
]
}
},
{
"id": "129362",
"type": "27",
"meta": {
"position": {
"x": 640,
"y": 0
}
},
"data": {
"nodeMeta": {
"title": "知识库写入",
"icon": "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-KnowledgeWriting-v2.jpg",
"description": "写入节点可以添加 文本类型 的知识库,仅可以添加一个知识库",
"mainColor": "#FF811A",
"subTitle": "知识库写入"
},
"outputs": [
{
"type": "string",
"name": "documentId"
},
{
"type": "string",
"name": "fileName"
},
{
"type": "string",
"name": "fileUrl"
}
],
"inputs": {
"inputParameters": [
{
"name": "knowledge",
"input": {
"type": "string",
"assistType": 1,
"value": {
"type": "literal",
"content": "http://coze.fanlv.fun:8889/opencoze/tos-cn-i-v4nquku3lp/f34d246d-7179-4f0d-856d-5d4c135ffee5.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20250910%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250910T074330Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=2208774d8ffc6f116c3f9851a184d851fbcb745ff78cd001359a873f19f6dad4&x-wf-file_name=%E5%8C%97%E4%BA%AC%E6%97%85%E6%B8%B8%E6%99%AF%E7%82%B9.txt",
"rawMeta": {
"fileName": "北京旅游景点.txt",
"type": 8
}
}
}
}
],
"datasetParam": [
{
"name": "datasetList",
"input": {
"type": "list",
"schema": {
"type": "string"
},
"value": {
"type": "literal",
"content": ["7548363002819379200"]
}
}
}
],
"strategyParam": {
"parsingStrategy": {
"parsingType": "fast"
},
"chunkStrategy": {
"chunkType": "default"
},
"indexStrategy": {}
}
}
}
},
{
"id": "164528",
"type": "27",
"meta": {
"position": {
"x": 1100,
"y": 0
}
},
"data": {
"nodeMeta": {
"title": "知识库写入_1",
"icon": "https://lf3-static.bytednsdoc.com/obj/eden-cn/dvsmryvd_avi_dvsm/ljhwZthlaukjlkulzlp/icon/icon-KnowledgeWriting-v2.jpg",
"description": "写入节点可以添加 文本类型 的知识库,仅可以添加一个知识库",
"mainColor": "#FF811A",
"subTitle": "知识库写入"
},
"outputs": [
{
"type": "string",
"name": "documentId"
},
{
"type": "string",
"name": "fileName"
},
{
"type": "string",
"name": "fileUrl"
}
],
"inputs": {
"inputParameters": [
{
"name": "knowledge",
"input": {
"type": "string",
"assistType": 1,
"value": {
"type": "literal",
"content": "http://coze.fanlv.fun:8889/opencoze/tos-cn-i-v4nquku3lp/b84062e4-d79e-4b3d-bf42-eaab754250df.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20250910%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250910T075358Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=346934ac110db0153afc1759c372e856afc9c5298c8c72e1210bba0c979f84cb&x-wf-file_name=%E5%8C%97%E4%BA%AC%E6%97%85%E6%B8%B8%E6%99%AF%E7%82%B9.txt",
"rawMeta": {
"fileName": "北京旅游景点.txt",
"type": 8
}
}
}
}
],
"datasetParam": [
{
"name": "datasetList",
"input": {
"type": "list",
"schema": {
"type": "string"
},
"value": {
"type": "literal",
"content": ["7548363002819379200"]
}
}
}
],
"strategyParam": {
"parsingStrategy": {
"parsingType": "fast"
},
"chunkStrategy": {
"chunkType": "default"
},
"indexStrategy": {}
}
}
}
}
],
"edges": [
{
"sourceNodeID": "100001",
"targetNodeID": "129362"
},
{
"sourceNodeID": "191034",
"targetNodeID": "900001"
},
{
"sourceNodeID": "164528",
"targetNodeID": "191034"
},
{
"sourceNodeID": "129362",
"targetNodeID": "164528"
}
],
"versions": {
"loop": "v2"
}
}

View File

@ -85,6 +85,7 @@ func init() {
_ = compose.RegisterSerializableType[*vo.TypeInfo]("type_info")
_ = compose.RegisterSerializableType[vo.DataType]("data_type")
_ = compose.RegisterSerializableType[vo.FileSubType]("file_sub_type")
_ = compose.RegisterSerializableType[*workflowModel.FileInfo]("file_info")
}
func (s *State) GetNodeCtx(key vo.NodeKey) (*execute.Context, bool, error) {

View File

@ -27,17 +27,17 @@ import (
func TestAddFieldMappingsWithDeduplication(t *testing.T) {
tests := []struct {
name string
name string
initialCarryOvers map[vo.NodeKey][]*compose.FieldMapping
fromNodeKey vo.NodeKey
fieldMappings []*compose.FieldMapping
expectedCount int
description string
fromNodeKey vo.NodeKey
fieldMappings []*compose.FieldMapping
expectedCount int
description string
}{
{
name: "empty_carry_overs",
name: "empty_carry_overs",
initialCarryOvers: make(map[vo.NodeKey][]*compose.FieldMapping),
fromNodeKey: "node1",
fromNodeKey: "node1",
fieldMappings: []*compose.FieldMapping{
compose.MapFieldPaths(compose.FieldPath{"input1"}, compose.FieldPath{"output1"}),
compose.MapFieldPaths(compose.FieldPath{"input2"}, compose.FieldPath{"output2"}),
@ -94,7 +94,7 @@ func TestAddFieldMappingsWithDeduplication(t *testing.T) {
description: "should not add any mappings when all are duplicates",
},
{
name: "new_node_key",
name: "new_node_key",
initialCarryOvers: map[vo.NodeKey][]*compose.FieldMapping{
"node1": {
compose.MapFieldPaths(compose.FieldPath{"input1"}, compose.FieldPath{"output1"}),
@ -109,7 +109,7 @@ func TestAddFieldMappingsWithDeduplication(t *testing.T) {
description: "should add all mappings for new node key",
},
{
name: "empty_field_mappings",
name: "empty_field_mappings",
initialCarryOvers: map[vo.NodeKey][]*compose.FieldMapping{
"node1": {
compose.MapFieldPaths(compose.FieldPath{"input1"}, compose.FieldPath{"output1"}),

View File

@ -25,6 +25,8 @@ import (
"github.com/cloudwego/eino/components/tool"
einoCompose "github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
workflowModel "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/workflow"
"github.com/coze-dev/coze-studio/backend/pkg/lang/slices"
wf "github.com/coze-dev/coze-studio/backend/domain/workflow"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
@ -91,6 +93,9 @@ func (wt *workflowTool) prepare(ctx context.Context, rInfo *entity.ResumeRequest
lastEventChan <-chan *execute.Event, callOpts []einoCompose.Option, err error) {
cfg := execute.GetExecuteConfig(opts...)
cfg.InputFileFields = slices.ToMap(wt.sc.GetAllNodesInputFileFields(ctx), func(e *workflowModel.FileInfo) (string, *workflowModel.FileInfo) {
return e.FileURL, e
})
var runOpts []WorkflowRunnerOption
if rInfo != nil && !rInfo.Resumed {
runOpts = append(runOpts, WithResumeReq(rInfo))
@ -121,12 +126,19 @@ func (wt *workflowTool) prepare(ctx context.Context, rInfo *entity.ResumeRequest
if entryNode == nil {
panic("entry node not found in tool workflow")
}
input, ws, err = nodes.ConvertInputs(ctx, input, entryNode.OutputTypes)
collectFileFields := make(map[string]*workflowModel.FileInfo, 0)
input, ws, err = nodes.ConvertInputs(ctx, input, entryNode.OutputTypes, nodes.WithCollectFileFields(collectFileFields))
if err != nil {
return
} else if ws != nil {
logs.CtxWarnf(ctx, "convert inputs warnings: %v", *ws)
}
if len(collectFileFields) > 0 {
for k, v := range collectFileFields {
cfg.InputFileFields[k] = v
}
}
}
cancelCtx, executeID, callOpts, lastEventChan, err = NewWorkflowRunner(wt.wfEntity.GetBasic(), wt.sc, cfg, runOpts...).Prepare(ctx)

View File

@ -148,7 +148,7 @@ func (ch *ConversationHistory) Invoke(ctx context.Context, input map[string]any)
runIDs, err := crossmessage.DefaultSVC().GetLatestRunIDs(ctx, &crossmessage.GetLatestRunIDsRequest{
ConversationID: conversationID,
UserID: userID,
AppID: *appID,
BizID: *appID,
Rounds: rounds,
InitRunID: initRunID,
SectionID: sectionID,

View File

@ -109,7 +109,7 @@ func (c *CreateConversation) Invoke(ctx context.Context, input map[string]any) (
if existed {
cID, _, existed, err := workflow.GetRepository().GetOrCreateStaticConversation(ctx, env, conversationIDGenerator, &vo.CreateStaticConversation{
AppID: ptr.From(appID),
BizID: ptr.From(appID),
TemplateID: template.TemplateID,
UserID: userID,
ConnectorID: connectorID,
@ -125,7 +125,7 @@ func (c *CreateConversation) Invoke(ctx context.Context, input map[string]any) (
}
cID, _, existed, err := workflow.GetRepository().GetOrCreateDynamicConversation(ctx, env, conversationIDGenerator, &vo.CreateDynamicConversation{
AppID: ptr.From(appID),
BizID: ptr.From(appID),
UserID: userID,
ConnectorID: connectorID,
Name: conversationName,

View File

@ -98,7 +98,7 @@ func (c *CreateMessage) getConversationIDByName(ctx context.Context, env vo.Env,
var conversationID int64
if isExist {
cID, _, _, err := workflow.GetRepository().GetOrCreateStaticConversation(ctx, env, conversationIDGenerator, &vo.CreateStaticConversation{
AppID: ptr.From(appID),
BizID: ptr.From(appID),
TemplateID: template.TemplateID,
UserID: userID,
ConnectorID: connectorID,
@ -150,7 +150,7 @@ func (c *CreateMessage) Invoke(ctx context.Context, input map[string]any) (map[s
var conversationID int64
var err error
var resolvedAppID int64
var bizID int64
if appID == nil {
if conversationName != "Default" {
return nil, vo.WrapError(errno.ErrOnlyDefaultConversationAllowInAgentScenario, errors.New("conversation node only allow in application"))
@ -167,13 +167,13 @@ func (c *CreateMessage) Invoke(ctx context.Context, input map[string]any) (map[s
}, nil
}
conversationID = *execCtx.ExeCfg.ConversationID
resolvedAppID = *agentID
bizID = *agentID
} else {
conversationID, err = c.getConversationIDByName(ctx, env, appID, version, conversationName, userID, connectorID)
if err != nil {
return nil, err
}
resolvedAppID = *appID
bizID = *appID
}
if conversationID == 0 {
@ -209,7 +209,7 @@ func (c *CreateMessage) Invoke(ctx context.Context, input map[string]any) (map[s
if role == "user" {
// For user messages, always create a new run and store the ID in the context.
runRecord, err := crossagentrun.DefaultSVC().Create(ctx, &agententity.AgentRunMeta{
AgentID: resolvedAppID,
AgentID: bizID,
ConversationID: conversationID,
UserID: strconv.FormatInt(userID, 10),
ConnectorID: connectorID,
@ -244,7 +244,7 @@ func (c *CreateMessage) Invoke(ctx context.Context, input map[string]any) (map[s
runIDs, err := crossmessage.DefaultSVC().GetLatestRunIDs(ctx, &crossmessage.GetLatestRunIDsRequest{
ConversationID: conversationID,
UserID: userID,
AppID: resolvedAppID,
BizID: bizID,
Rounds: 1,
})
if err != nil {
@ -254,7 +254,7 @@ func (c *CreateMessage) Invoke(ctx context.Context, input map[string]any) (map[s
runID = runIDs[0]
} else {
runRecord, err := crossagentrun.DefaultSVC().Create(ctx, &agententity.AgentRunMeta{
AgentID: resolvedAppID,
AgentID: bizID,
ConversationID: conversationID,
UserID: strconv.FormatInt(userID, 10),
ConnectorID: connectorID,
@ -273,7 +273,7 @@ func (c *CreateMessage) Invoke(ctx context.Context, input map[string]any) (map[s
Content: content,
ContentType: model.ContentType("text"),
UserID: strconv.FormatInt(userID, 10),
AgentID: resolvedAppID,
AgentID: bizID,
RunID: runID,
SectionID: sectionID,
}

View File

@ -115,7 +115,7 @@ func (m *MessageList) Invoke(ctx context.Context, input map[string]any) (map[str
var conversationID int64
var err error
var resolvedAppID int64
var bizID int64
if appID == nil {
if conversationName != "Default" {
return nil, vo.WrapError(errno.ErrOnlyDefaultConversationAllowInAgentScenario, errors.New("conversation node only allow in application"))
@ -129,18 +129,18 @@ func (m *MessageList) Invoke(ctx context.Context, input map[string]any) (map[str
}, nil
}
conversationID = *execCtx.ExeCfg.ConversationID
resolvedAppID = *agentID
bizID = *agentID
} else {
conversationID, err = m.getConversationIDByName(ctx, env, appID, version, conversationName, userID, connectorID)
if err != nil {
return nil, err
}
resolvedAppID = *appID
bizID = *appID
}
req := &crossmessage.MessageListRequest{
UserID: userID,
AppID: resolvedAppID,
BizID: bizID,
ConversationID: conversationID,
}

View File

@ -20,9 +20,12 @@ import (
"context"
"errors"
"fmt"
"net/url"
"path/filepath"
"strconv"
"strings"
workflowModel "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/workflow"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
@ -127,9 +130,23 @@ func ConvertInputs(ctx context.Context, in map[string]any, tInfo map[string]*vo.
}
type convertOptions struct {
skipUnknownFields bool
failFast bool
skipRequireCheck bool
skipUnknownFields bool
failFast bool
skipRequireCheck bool
collectFileFields map[string]*workflowModel.FileInfo
notNeedTrimQueryFileName bool
}
func WithCollectFileFields(fs map[string]*workflowModel.FileInfo) ConvertOption {
return func(o *convertOptions) {
o.collectFileFields = fs
}
}
func WithNotNeedTrimQueryFileName(b bool) ConvertOption {
return func(o *convertOptions) {
o.notNeedTrimQueryFileName = b
}
}
type ConvertOption func(*convertOptions)
@ -161,6 +178,23 @@ func Convert(ctx context.Context, in any, path string, t *vo.TypeInfo, opts ...C
return convert(ctx, in, path, t, options)
}
func adaptorFileURL(in string) (string, *workflowModel.FileInfo, error) {
u, err := url.Parse(in)
if err != nil {
return "", nil, err
}
query := u.Query()
fileName := query.Get("x-wf-file_name")
fileInfo := &workflowModel.FileInfo{
FileName: fileName,
FileExtension: filepath.Ext(fileName),
}
query.Del("x-wf-file_name")
u.RawQuery = query.Encode()
fileInfo.FileURL = u.String()
return u.String(), fileInfo, nil
}
func convert(ctx context.Context, in any, path string, t *vo.TypeInfo, options *convertOptions) (
any, *ConversionWarnings, error) {
if in == nil { // nil is valid for ALL types
@ -168,8 +202,28 @@ func convert(ctx context.Context, in any, path string, t *vo.TypeInfo, options *
}
switch t.Type {
case vo.DataTypeString, vo.DataTypeFile, vo.DataTypeTime:
case vo.DataTypeString, vo.DataTypeTime:
return convertToString(ctx, in, path, options)
case vo.DataTypeFile:
ret, warns, err := convertToString(ctx, in, path, options)
if err != nil {
return nil, nil, err
}
if warns != nil {
return ret, warns, nil
}
fileURL, fileInfo, err := adaptorFileURL(ret.(string))
if err != nil {
return nil, nil, err
}
if options.collectFileFields != nil {
options.collectFileFields[fileInfo.FileURL] = fileInfo
}
if options.notNeedTrimQueryFileName {
return ret, nil, nil
}
return fileURL, nil, nil
case vo.DataTypeInteger:
return convertToInt64(ctx, in, path, options)
case vo.DataTypeNumber:

View File

@ -82,7 +82,9 @@ func (c *Config) Adapt(_ context.Context, n *vo.Node, _ ...nodes.AdaptOption) (*
return nil, fmt.Errorf("exit node's content value type must be %s, got %s", vo.BlockInputValueTypeLiteral, content.Value.Type)
}
c.Template = content.Value.Content.(string)
if content.Value.Content != nil {
c.Template = content.Value.Content.(string)
}
}
if n.Data.Inputs.TerminatePlan == nil {

View File

@ -20,8 +20,9 @@ import (
"context"
"errors"
"fmt"
"net/url"
"path/filepath"
"net/url"
"strings"
"github.com/spf13/cast"
@ -31,6 +32,7 @@ import (
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/canvas/convert"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/execute"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/nodes"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/schema"
"github.com/coze-dev/coze-studio/backend/infra/contract/document/parser"
@ -125,7 +127,7 @@ func (k *Indexer) Invoke(ctx context.Context, input map[string]any) (map[string]
return nil, errors.New("knowledge is required")
}
fileName, ext, err := parseToFileNameAndFileExtension(fileURL)
fileName, ext, err := parseToFileNameAndFileExtension(ctx, fileURL)
if err != nil {
return nil, err
@ -153,23 +155,25 @@ func (k *Indexer) Invoke(ctx context.Context, input map[string]any) (map[string]
return result, nil
}
func parseToFileNameAndFileExtension(fileURL string) (string, parser.FileExtension, error) {
u, err := url.Parse(fileURL)
if err != nil {
return "", "", err
func parseToFileNameAndFileExtension(ctx context.Context, fileURL string) (string, parser.FileExtension, error) {
inputFileFields := execute.GetExeCtx(ctx).ExeCfg.InputFileFields
fileInfo, ok := inputFileFields[fileURL]
if !ok {
u, err := url.Parse(fileURL)
if err != nil {
return "", "", err
}
fileExt := strings.TrimPrefix(strings.ToLower(filepath.Ext(u.Path)), ".")
ext, support := parser.ValidateFileExtension(fileExt)
if !support {
return "", "", fmt.Errorf("unsupported file type: %s", fileExt)
}
return u.Path, ext, nil
}
fileName := u.Query().Get("x-wf-file_name")
if len(fileName) == 0 {
return "", "", errors.New("file name is required")
}
fileExt := strings.ToLower(strings.TrimPrefix(filepath.Ext(fileName), "."))
ext, support := parser.ValidateFileExtension(fileExt)
ext, support := parser.ValidateFileExtension(strings.ToLower(strings.TrimPrefix(fileInfo.FileExtension, ".")))
if !support {
return "", "", fmt.Errorf("unsupported file type: %s", fileExt)
return "", "", fmt.Errorf("unsupported file type: %s", fileInfo.FileExtension)
}
return fileName, ext, nil
return fileInfo.FileName, ext, nil
}

View File

@ -100,9 +100,9 @@ const (
ReasoningOutputKey = "reasoning_content"
)
const knowledgeUserPromptTemplate = `根据引用的内容回答问题:
1.如果引用的内容里面包含 <img src=""> 的标签, 标签里的 src 字段表示图片地址, 需要在回答问题的时候展示出去, 输出格式为"![图片名称](图片地址)" 。
2.如果引用的内容不包含 <img src=""> 的标签, 你回答问题时不需要展示图片 。
const knowledgeUserPromptTemplate = `根据引用的内容回答问题:
1.如果引用的内容里面包含 <img src=""> 的标签, 标签里的 src 字段表示图片地址, 需要在回答问题的时候展示出去, 输出格式为"![图片名称](图片地址)" 。
2.如果引用的内容不包含 <img src=""> 的标签, 你回答问题时不需要展示图片 。
例如:
如果内容为<img src="https://example.com/image.jpg">一只小猫,你的输出应为:![一只小猫](https://example.com/image.jpg)。
如果内容为<img src="https://example.com/image1.jpg">一只小猫 和 <img src="https://example.com/image2.jpg">一只小狗 和 <img src="https://example.com/image3.jpg">一只小牛,你的输出应为:![一只小猫](https://example.com/image1.jpg) 和 ![一只小狗](https://example.com/image2.jpg) 和 ![一只小牛](https://example.com/image3.jpg)
@ -290,7 +290,7 @@ func (c *Config) Adapt(_ context.Context, n *vo.Node, _ ...nodes.AdaptOption) (*
c.AssociateStartNodeUserInputFields = make(map[string]struct{})
for _, info := range ns.InputSources {
if len(info.Path) == 1 && info.Source.Ref != nil && info.Source.Ref.FromNodeKey == entity.EntryNodeKey {
if compose.FromFieldPath(info.Source.Ref.FromPath).Equals(compose.FromField("USER_INPUT")) {
if compose.FromFieldPath(info.Source.Ref.FromPath).Equals(compose.FromField(vo.UserInputKey)) {
c.AssociateStartNodeUserInputFields[info.Path[0]] = struct{}{}
}
}
@ -1115,7 +1115,7 @@ func (l *LLM) handleInterrupt(ctx context.Context, err error, resumingEvent *ent
NodeKey: c.NodeKey,
NodeType: entity.NodeTypeLLM,
NodeTitle: c.NodeName,
NodeIcon: entity.NodeMetaByNodeType(entity.NodeTypeLLM).IconURL,
NodeIcon: entity.NodeMetaByNodeType(entity.NodeTypeLLM).IconURI,
EventType: entity.InterruptEventLLM,
}

View File

@ -192,8 +192,3 @@ type StreamGenerator interface {
FieldStreamType(path compose.FieldPath, ns *schema.NodeSchema,
sc *schema.WorkflowSchema) (schema.FieldStreamType, error)
}
type ChatHistoryAware interface {
ChatHistoryEnabled() bool
ChatHistoryRounds() int64
}

View File

@ -343,14 +343,14 @@ const (
你是一个参数提取 agent你的工作是从用户的回答中提取出多个字段的值每个字段遵循以下规则
# 字段说明
%s
## 输出要求
- 严格以 json 格式返回答案。
- 严格确保答案采用有效的 JSON 格式。
- 按照字段说明提取出字段的值,将已经提取到的字段放在 fields 字段
- 对于未提取到的<必填字段>生成一个新的追问问题question
- 确保在追问问题中只包含所有未提取的<必填字段>
- 不要重复问之前问过的问题
- 问题的语种请和用户的输入保持一致,如英文、中文等
## 输出要求
- 严格以 json 格式返回答案。
- 严格确保答案采用有效的 JSON 格式。
- 按照字段说明提取出字段的值,将已经提取到的字段放在 fields 字段
- 对于未提取到的<必填字段>生成一个新的追问问题question
- 确保在追问问题中只包含所有未提取的<必填字段>
- 不要重复问之前问过的问题
- 问题的语种请和用户的输入保持一致,如英文、中文等
- 输出按照下面结构体格式返回,包含提取到的字段或者追问的问题
- 不要回复和提取无关的问题
type Output struct {
@ -359,7 +359,7 @@ question string // Follow-up question for the next round
}`
extractUserPromptSuffix = `
- 严格以 json 格式返回答案。
- 严格确保答案采用有效的 JSON 格式。
- 严格确保答案采用有效的 JSON 格式。
- - 必填字段没有获取全则继续追问
- 必填字段: %s
%s
@ -728,7 +728,7 @@ func (q *QuestionAnswer) interrupt(ctx context.Context, newQuestion string, choi
NodeKey: q.nodeKey,
NodeType: entity.NodeTypeQuestionAnswer,
NodeTitle: q.nodeMeta.Name,
NodeIcon: q.nodeMeta.IconURL,
NodeIcon: q.nodeMeta.IconURI,
InterruptData: interruptData,
EventType: entity.InterruptEventQuestion,
}

View File

@ -140,7 +140,7 @@ func (i *InputReceiver) Invoke(ctx context.Context, _ map[string]any) (map[strin
NodeKey: i.nodeKey,
NodeType: entity.NodeTypeInputReceiver,
NodeTitle: i.nodeMeta.Name,
NodeIcon: i.nodeMeta.IconURL,
NodeIcon: i.nodeMeta.IconURI,
InterruptData: i.interruptData,
EventType: entity.InterruptEventInput,
})

View File

@ -432,7 +432,7 @@ func (r *RepositoryImpl) GetOrCreateDynamicConversation(ctx context.Context, env
appDynamicConversationDraft := r.query.AppDynamicConversationDraft
ret, err := appDynamicConversationDraft.WithContext(ctx).Where(
appDynamicConversationDraft.AppID.Eq(meta.AppID),
appDynamicConversationDraft.AppID.Eq(meta.BizID),
appDynamicConversationDraft.ConnectorID.Eq(meta.ConnectorID),
appDynamicConversationDraft.UserID.Eq(meta.UserID),
appDynamicConversationDraft.Name.Eq(meta.Name),
@ -452,7 +452,7 @@ func (r *RepositoryImpl) GetOrCreateDynamicConversation(ctx context.Context, env
return 0, 0, false, vo.WrapError(errno.ErrDatabaseError, err)
}
conv, err := idGen(ctx, meta.AppID, meta.UserID, meta.ConnectorID)
conv, err := idGen(ctx, meta.BizID, meta.UserID, meta.ConnectorID)
if err != nil {
return 0, 0, false, err
}
@ -464,7 +464,7 @@ func (r *RepositoryImpl) GetOrCreateDynamicConversation(ctx context.Context, env
err = r.query.AppDynamicConversationDraft.WithContext(ctx).Create(&model.AppDynamicConversationDraft{
ID: id,
AppID: meta.AppID,
AppID: meta.BizID,
Name: meta.Name,
UserID: meta.UserID,
ConnectorID: meta.ConnectorID,
@ -479,7 +479,7 @@ func (r *RepositoryImpl) GetOrCreateDynamicConversation(ctx context.Context, env
} else if env == vo.Online {
appDynamicConversationOnline := r.query.AppDynamicConversationOnline
ret, err := appDynamicConversationOnline.WithContext(ctx).Where(
appDynamicConversationOnline.AppID.Eq(meta.AppID),
appDynamicConversationOnline.AppID.Eq(meta.BizID),
appDynamicConversationOnline.ConnectorID.Eq(meta.ConnectorID),
appDynamicConversationOnline.UserID.Eq(meta.UserID),
appDynamicConversationOnline.Name.Eq(meta.Name),
@ -498,7 +498,7 @@ func (r *RepositoryImpl) GetOrCreateDynamicConversation(ctx context.Context, env
return 0, 0, false, vo.WrapError(errno.ErrDatabaseError, err)
}
conv, err := idGen(ctx, meta.AppID, meta.UserID, meta.ConnectorID)
conv, err := idGen(ctx, meta.BizID, meta.UserID, meta.ConnectorID)
if err != nil {
return 0, 0, false, err
}
@ -509,7 +509,7 @@ func (r *RepositoryImpl) GetOrCreateDynamicConversation(ctx context.Context, env
err = r.query.AppDynamicConversationOnline.WithContext(ctx).Create(&model.AppDynamicConversationOnline{
ID: id,
AppID: meta.AppID,
AppID: meta.BizID,
Name: meta.Name,
UserID: meta.UserID,
ConnectorID: meta.ConnectorID,
@ -586,7 +586,7 @@ func (r *RepositoryImpl) getOrCreateDraftStaticConversation(ctx context.Context,
return cs[0].ConversationID, cInfo.SectionID, true, nil
}
conv, err := idGen(ctx, meta.AppID, meta.UserID, meta.ConnectorID)
conv, err := idGen(ctx, meta.BizID, meta.UserID, meta.ConnectorID)
if err != nil {
return 0, 0, false, err
}
@ -627,7 +627,7 @@ func (r *RepositoryImpl) getOrCreateOnlineStaticConversation(ctx context.Context
return cs[0].ConversationID, cInfo.SectionID, true, nil
}
conv, err := idGen(ctx, meta.AppID, meta.UserID, meta.ConnectorID)
conv, err := idGen(ctx, meta.BizID, meta.UserID, meta.ConnectorID)
if err != nil {
return 0, 0, false, err
}
@ -841,7 +841,7 @@ func (r *RepositoryImpl) CopyTemplateConversationByAppID(ctx context.Context, ap
}
func (r *RepositoryImpl) GetStaticConversationByID(ctx context.Context, env vo.Env, appID, connectorID, conversationID int64) (string, bool, error) {
func (r *RepositoryImpl) GetStaticConversationByID(ctx context.Context, env vo.Env, bizID, connectorID, conversationID int64) (string, bool, error) {
if env == vo.Draft {
appStaticConversationDraft := r.query.AppStaticConversationDraft
ret, err := appStaticConversationDraft.WithContext(ctx).Where(
@ -857,7 +857,7 @@ func (r *RepositoryImpl) GetStaticConversationByID(ctx context.Context, env vo.E
appConversationTemplateDraft := r.query.AppConversationTemplateDraft
template, err := appConversationTemplateDraft.WithContext(ctx).Where(
appConversationTemplateDraft.TemplateID.Eq(ret.TemplateID),
appConversationTemplateDraft.AppID.Eq(appID),
appConversationTemplateDraft.AppID.Eq(bizID),
).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@ -881,7 +881,7 @@ func (r *RepositoryImpl) GetStaticConversationByID(ctx context.Context, env vo.E
appConversationTemplateOnline := r.query.AppConversationTemplateOnline
template, err := appConversationTemplateOnline.WithContext(ctx).Where(
appConversationTemplateOnline.TemplateID.Eq(ret.TemplateID),
appConversationTemplateOnline.AppID.Eq(appID),
appConversationTemplateOnline.AppID.Eq(bizID),
).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@ -894,11 +894,11 @@ func (r *RepositoryImpl) GetStaticConversationByID(ctx context.Context, env vo.E
return "", false, fmt.Errorf("unknown env %v", env)
}
func (r *RepositoryImpl) GetDynamicConversationByID(ctx context.Context, env vo.Env, appID, connectorID, conversationID int64) (*entity.DynamicConversation, bool, error) {
func (r *RepositoryImpl) GetDynamicConversationByID(ctx context.Context, env vo.Env, bizID, connectorID, conversationID int64) (*entity.DynamicConversation, bool, error) {
if env == vo.Draft {
appDynamicConversationDraft := r.query.AppDynamicConversationDraft
ret, err := appDynamicConversationDraft.WithContext(ctx).Where(
appDynamicConversationDraft.AppID.Eq(appID),
appDynamicConversationDraft.AppID.Eq(bizID),
appDynamicConversationDraft.ConnectorID.Eq(connectorID),
appDynamicConversationDraft.ConversationID.Eq(conversationID),
).First()
@ -918,7 +918,7 @@ func (r *RepositoryImpl) GetDynamicConversationByID(ctx context.Context, env vo.
} else if env == vo.Online {
appDynamicConversationOnline := r.query.AppDynamicConversationOnline
ret, err := appDynamicConversationOnline.WithContext(ctx).Where(
appDynamicConversationOnline.AppID.Eq(appID),
appDynamicConversationOnline.AppID.Eq(bizID),
appDynamicConversationOnline.ConnectorID.Eq(connectorID),
appDynamicConversationOnline.ConversationID.Eq(conversationID),
).First()

View File

@ -129,3 +129,8 @@ func (s *NodeSchema) SetOutputType(key string, t *vo.TypeInfo) {
func (s *NodeSchema) AddOutputSource(info ...*vo.FieldInfo) {
s.OutputSources = append(s.OutputSources, info...)
}
type ChatHistoryAware interface {
ChatHistoryEnabled() bool
ChatHistoryRounds() int64
}

View File

@ -17,13 +17,19 @@
package schema
import (
"context"
"fmt"
"maps"
"net/url"
"path/filepath"
"reflect"
"strings"
"sync"
workflowModel "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/workflow"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
)
type WorkflowSchema struct {
@ -38,6 +44,7 @@ type WorkflowSchema struct {
compositeNodes []*CompositeNode // won't serialize this
requireCheckPoint bool // won't serialize this
requireStreaming bool
historyRounds int64
once sync.Once
}
@ -69,15 +76,22 @@ func (w *WorkflowSchema) Init() {
w.doGetCompositeNodes()
historyRounds := int64(0)
for _, node := range w.Nodes {
if node.Type == entity.NodeTypeSubWorkflow {
node.SubWorkflowSchema.Init()
historyRounds = max(historyRounds, node.SubWorkflowSchema.HistoryRounds())
if node.SubWorkflowSchema.requireCheckPoint {
w.requireCheckPoint = true
break
}
}
chatHistoryAware, ok := node.Configs.(ChatHistoryAware)
if ok && chatHistoryAware.ChatHistoryEnabled() {
historyRounds = max(historyRounds, chatHistoryAware.ChatHistoryRounds())
}
if rc, ok := node.Configs.(RequireCheckpoint); ok {
if rc.RequireCheckpoint() {
w.requireCheckPoint = true
@ -86,6 +100,7 @@ func (w *WorkflowSchema) Init() {
}
}
w.historyRounds = historyRounds
w.requireStreaming = w.doRequireStreaming()
})
}
@ -122,6 +137,12 @@ func (w *WorkflowSchema) RequireStreaming() bool {
return w.requireStreaming
}
func (w *WorkflowSchema) HistoryRounds() int64 { return w.historyRounds }
func (w *WorkflowSchema) SetHistoryRounds(historyRounds int64) {
w.historyRounds = historyRounds
}
func (w *WorkflowSchema) doGetCompositeNodes() (cNodes []*CompositeNode) {
if w.Hierarchy == nil {
return nil
@ -307,3 +328,65 @@ func (w *WorkflowSchema) doRequireStreaming() bool {
return false
}
func (w *WorkflowSchema) GetAllNodesInputFileFields(ctx context.Context) []*workflowModel.FileInfo {
adaptorURL := func(s string) (string, error) {
u, err := url.Parse(s)
if err != nil {
return "", err
}
query := u.Query()
query.Del("x-wf-file_name")
u.RawQuery = query.Encode()
return u.String(), nil
}
result := make([]*workflowModel.FileInfo, 0)
for _, node := range w.Nodes {
for _, source := range node.InputSources {
if source.Source.Val != nil && source.Source.FileExtra != nil {
fileExtra := source.Source.FileExtra
if fileExtra.FileName != nil {
fileURL, err := adaptorURL(source.Source.Val.(string))
if err != nil {
logs.CtxWarnf(ctx, "failed to parse adaptorURL for node %v: %v", node.Key, err)
continue
}
result = append(result, &workflowModel.FileInfo{
FileName: *fileExtra.FileName,
FileURL: fileURL,
FileExtension: filepath.Ext(strings.TrimSpace(*fileExtra.FileName)),
})
source.Source.Val = fileURL
}
if fileExtra.FileNames != nil {
vals := source.Source.Val.([]any)
for idx, fileName := range fileExtra.FileNames {
fileURL := vals[idx].(string)
fileURL, err := adaptorURL(fileURL)
if err != nil {
logs.CtxWarnf(ctx, "failed to parse adaptorURL for node %v: %v", node.Key, err)
continue
}
result = append(result, &workflowModel.FileInfo{
FileName: fileName,
FileURL: fileURL,
FileExtension: filepath.Ext(strings.TrimSpace(fileName)),
})
vals[idx] = fileURL
}
source.Source.Val = vals
}
}
}
if node.SubWorkflowSchema != nil {
result = append(result, node.SubWorkflowSchema.GetAllNodesInputFileFields(ctx)...)
}
}
return result
}

View File

@ -248,7 +248,7 @@ func (c *conversationImpl) findReplaceWorkflowByConversationName(ctx context.Con
if err != nil {
return false, err
}
if v.Name == "CONVERSATION_NAME" && v.DefaultValue == name {
if v.Name == vo.ConversationNameKey && v.DefaultValue == name {
return true, nil
}
}
@ -296,7 +296,7 @@ func (c *conversationImpl) replaceWorkflowsConversationName(ctx context.Context,
if err != nil {
return err
}
if v.Name == "CONVERSATION_NAME" {
if v.Name == vo.ConversationNameKey {
v.DefaultValue = conversionName
}
startNode.Data.Outputs[idx] = v
@ -351,18 +351,18 @@ func (c *conversationImpl) DeleteDynamicConversation(ctx context.Context, env vo
return c.repo.DeleteDynamicConversation(ctx, env, templateID)
}
func (c *conversationImpl) GetOrCreateConversation(ctx context.Context, env vo.Env, appID, connectorID, userID int64, conversationName string) (int64, int64, error) {
func (c *conversationImpl) GetOrCreateConversation(ctx context.Context, env vo.Env, bizID, connectorID, userID int64, conversationName string) (int64, int64, error) {
t, existed, err := c.repo.GetConversationTemplate(ctx, env, vo.GetConversationTemplatePolicy{
AppID: ptr.Of(appID),
AppID: ptr.Of(bizID),
Name: ptr.Of(conversationName),
})
if err != nil {
return 0, 0, err
}
conversationIDGenerator := workflow.ConversationIDGenerator(func(ctx context.Context, appID int64, userID, connectorID int64) (*conventity.Conversation, error) {
conversationIDGenerator := workflow.ConversationIDGenerator(func(ctx context.Context, bizID int64, userID, connectorID int64) (*conventity.Conversation, error) {
return crossconversation.DefaultSVC().CreateConversation(ctx, &conventity.CreateMeta{
AgentID: appID,
AgentID: bizID,
UserID: userID,
ConnectorID: connectorID,
Scene: common.Scene_SceneWorkflow,
@ -371,7 +371,7 @@ func (c *conversationImpl) GetOrCreateConversation(ctx context.Context, env vo.E
if existed {
conversationID, sectionID, _, err := c.repo.GetOrCreateStaticConversation(ctx, env, conversationIDGenerator, &vo.CreateStaticConversation{
AppID: appID,
BizID: bizID,
ConnectorID: connectorID,
UserID: userID,
TemplateID: t.TemplateID,
@ -383,7 +383,7 @@ func (c *conversationImpl) GetOrCreateConversation(ctx context.Context, env vo.E
}
conversationID, sectionID, _, err := c.repo.GetOrCreateDynamicConversation(ctx, env, conversationIDGenerator, &vo.CreateDynamicConversation{
AppID: appID,
BizID: bizID,
ConnectorID: connectorID,
UserID: userID,
Name: conversationName,
@ -465,8 +465,8 @@ func (c *conversationImpl) GetDynamicConversationByName(ctx context.Context, env
return c.repo.GetDynamicConversationByName(ctx, env, appID, connectorID, userID, name)
}
func (c *conversationImpl) GetConversationNameByID(ctx context.Context, env vo.Env, appID, connectorID, conversationID int64) (string, bool, error) {
sc, existed, err := c.repo.GetStaticConversationByID(ctx, env, appID, connectorID, conversationID)
func (c *conversationImpl) GetConversationNameByID(ctx context.Context, env vo.Env, bizID, connectorID, conversationID int64) (string, bool, error) {
sc, existed, err := c.repo.GetStaticConversationByID(ctx, env, bizID, connectorID, conversationID)
if err != nil {
return "", false, err
}
@ -474,7 +474,7 @@ func (c *conversationImpl) GetConversationNameByID(ctx context.Context, env vo.E
return sc, true, nil
}
dc, existed, err := c.repo.GetDynamicConversationByID(ctx, env, appID, connectorID, conversationID)
dc, existed, err := c.repo.GetDynamicConversationByID(ctx, env, bizID, connectorID, conversationID)
if err != nil {
return "", false, err
}

View File

@ -22,6 +22,8 @@ import (
"fmt"
"time"
"github.com/coze-dev/coze-studio/backend/types/consts"
einoCompose "github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
@ -84,6 +86,9 @@ func (i *impl) SyncExecute(ctx context.Context, config workflowModel.ExecuteConf
return nil, "", fmt.Errorf("failed to convert canvas to workflow schema: %w", err)
}
config.InputFileFields = slices.ToMap(workflowSC.GetAllNodesInputFileFields(ctx), func(e *workflowModel.FileInfo) (string, *workflowModel.FileInfo) {
return e.FileURL, e
})
var wfOpts []compose.WorkflowOption
wfOpts = append(wfOpts, compose.WithIDAsName(wfEntity.ID))
if s := execute.GetStaticConfig(); s != nil && s.MaxNodeCountPerWorkflow > 0 {
@ -100,6 +105,8 @@ func (i *impl) SyncExecute(ctx context.Context, config workflowModel.ExecuteConf
}
var cOpts []nodes.ConvertOption
inputFileFields := make(map[string]*workflowModel.FileInfo)
cOpts = append(cOpts, nodes.WithCollectFileFields(inputFileFields), nodes.WithNotNeedTrimQueryFileName(true))
if config.InputFailFast {
cOpts = append(cOpts, nodes.FailFast())
}
@ -111,6 +118,10 @@ func (i *impl) SyncExecute(ctx context.Context, config workflowModel.ExecuteConf
logs.CtxWarnf(ctx, "convert inputs warnings: %v", *ws)
}
for k, v := range inputFileFields {
config.InputFileFields[k] = v
}
inStr, err := sonic.MarshalString(input)
if err != nil {
return nil, "", err
@ -231,6 +242,10 @@ func (i *impl) AsyncExecute(ctx context.Context, config workflowModel.ExecuteCon
return 0, fmt.Errorf("failed to convert canvas to workflow schema: %w", err)
}
config.InputFileFields = slices.ToMap(workflowSC.GetAllNodesInputFileFields(ctx), func(e *workflowModel.FileInfo) (string, *workflowModel.FileInfo) {
return e.FileURL, e
})
var wfOpts []compose.WorkflowOption
wfOpts = append(wfOpts, compose.WithIDAsName(wfEntity.ID))
if s := execute.GetStaticConfig(); s != nil && s.MaxNodeCountPerWorkflow > 0 {
@ -249,6 +264,8 @@ func (i *impl) AsyncExecute(ctx context.Context, config workflowModel.ExecuteCon
config.CommitID = wfEntity.CommitID
var cOpts []nodes.ConvertOption
inputFileFields := make(map[string]*workflowModel.FileInfo)
cOpts = append(cOpts, nodes.WithCollectFileFields(inputFileFields), nodes.WithNotNeedTrimQueryFileName(true))
if config.InputFailFast {
cOpts = append(cOpts, nodes.FailFast())
}
@ -260,6 +277,10 @@ func (i *impl) AsyncExecute(ctx context.Context, config workflowModel.ExecuteCon
logs.CtxWarnf(ctx, "convert inputs warnings: %v", *ws)
}
for k, v := range inputFileFields {
config.InputFileFields[k] = v
}
inStr, err := sonic.MarshalString(input)
if err != nil {
return 0, err
@ -282,6 +303,50 @@ func (i *impl) AsyncExecute(ctx context.Context, config workflowModel.ExecuteCon
return executeID, nil
}
func (i *impl) handleHistory(ctx context.Context, config *workflowModel.ExecuteConfig, input map[string]any, historyRounds int64, shouldFetchConversationByName bool) error {
if historyRounds <= 0 {
return nil
}
if shouldFetchConversationByName {
var cID, sID, bizID int64
var err error
if config.AppID != nil {
bizID = *config.AppID
} else if config.AgentID != nil {
bizID = *config.AgentID
}
for k, v := range input {
if k == vo.ConversationNameKey {
cName, ok := v.(string)
if !ok {
return errors.New("CONVERSATION_NAME must be string")
}
cID, sID, err = i.GetOrCreateConversation(ctx, vo.Draft, bizID, consts.CozeConnectorID, config.Operator, cName)
if err != nil {
return err
}
config.ConversationID = ptr.Of(cID)
config.SectionID = ptr.Of(sID)
}
}
}
messages, scMessages, err := i.prefetchChatHistory(ctx, *config, historyRounds)
if err != nil {
logs.CtxErrorf(ctx, "failed to prefetch chat history: %v", err)
}
if len(messages) > 0 {
config.ConversationHistory = messages
}
if len(scMessages) > 0 {
config.ConversationHistorySchemaMessages = scMessages
}
return nil
}
func (i *impl) AsyncExecuteNode(ctx context.Context, nodeID string, config workflowModel.ExecuteConfig, input map[string]any) (int64, error) {
var (
err error
@ -308,30 +373,6 @@ func (i *impl) AsyncExecuteNode(ctx context.Context, nodeID string, config workf
}
}
historyRounds := int64(0)
if config.WorkflowMode == workflowapimodel.WorkflowMode_ChatFlow {
historyRounds, err = getHistoryRoundsFromNode(ctx, wfEntity, nodeID, i.repo)
if err != nil {
return 0, err
}
}
if historyRounds > 0 {
messages, scMessages, err := i.prefetchChatHistory(ctx, config, historyRounds)
if err != nil {
logs.CtxErrorf(ctx, "failed to prefetch chat history: %v", err)
}
if len(messages) > 0 {
config.ConversationHistory = messages
}
if len(scMessages) > 0 {
config.ConversationHistorySchemaMessages = scMessages
}
}
c := &vo.Canvas{}
if err = sonic.UnmarshalString(wfEntity.Canvas, c); err != nil {
return 0, fmt.Errorf("failed to unmarshal canvas: %w", err)
@ -342,12 +383,27 @@ func (i *impl) AsyncExecuteNode(ctx context.Context, nodeID string, config workf
return 0, fmt.Errorf("failed to convert canvas to workflow schema: %w", err)
}
historyRounds := int64(0)
if config.WorkflowMode == workflowapimodel.WorkflowMode_ChatFlow {
historyRounds = workflowSC.HistoryRounds()
}
if historyRounds > 0 {
if err = i.handleHistory(ctx, &config, input, historyRounds, true); err != nil {
return 0, err
}
}
config.InputFileFields = slices.ToMap(workflowSC.GetAllNodesInputFileFields(ctx), func(e *workflowModel.FileInfo) (string, *workflowModel.FileInfo) {
return e.FileURL, e
})
wf, err := compose.NewWorkflowFromNode(ctx, workflowSC, vo.NodeKey(nodeID), einoCompose.WithGraphName(fmt.Sprintf("%d", wfEntity.ID)))
if err != nil {
return 0, fmt.Errorf("failed to create workflow: %w", err)
}
var cOpts []nodes.ConvertOption
inputFileFields := make(map[string]*workflowModel.FileInfo)
cOpts = append(cOpts, nodes.WithCollectFileFields(inputFileFields), nodes.WithNotNeedTrimQueryFileName(true))
if config.InputFailFast {
cOpts = append(cOpts, nodes.FailFast())
}
@ -358,6 +414,9 @@ func (i *impl) AsyncExecuteNode(ctx context.Context, nodeID string, config workf
} else if ws != nil {
logs.CtxWarnf(ctx, "convert inputs warnings: %v", *ws)
}
for k, v := range inputFileFields {
config.InputFileFields[k] = v
}
if wfEntity.AppID != nil && config.AppID == nil {
config.AppID = wfEntity.AppID
@ -417,29 +476,6 @@ func (i *impl) StreamExecute(ctx context.Context, config workflowModel.ExecuteCo
}
}
historyRounds := int64(0)
if config.WorkflowMode == workflowapimodel.WorkflowMode_ChatFlow {
historyRounds, err = i.calculateMaxChatHistoryRounds(ctx, wfEntity, i.repo)
if err != nil {
return nil, err
}
}
if historyRounds > 0 {
messages, scMessages, err := i.prefetchChatHistory(ctx, config, historyRounds)
if err != nil {
logs.CtxErrorf(ctx, "failed to prefetch chat history: %v", err)
}
if len(messages) > 0 {
config.ConversationHistory = messages
}
if len(scMessages) > 0 {
config.ConversationHistorySchemaMessages = scMessages
}
}
c := &vo.Canvas{}
if err = sonic.UnmarshalString(wfEntity.Canvas, c); err != nil {
return nil, fmt.Errorf("failed to unmarshal canvas: %w", err)
@ -450,7 +486,23 @@ func (i *impl) StreamExecute(ctx context.Context, config workflowModel.ExecuteCo
return nil, fmt.Errorf("failed to convert canvas to workflow schema: %w", err)
}
historyRounds := int64(0)
if config.WorkflowMode == workflowapimodel.WorkflowMode_ChatFlow {
historyRounds = workflowSC.HistoryRounds()
}
if historyRounds > 0 {
if err = i.handleHistory(ctx, &config, input, historyRounds, false); err != nil {
return nil, err
}
}
config.InputFileFields = slices.ToMap(workflowSC.GetAllNodesInputFileFields(ctx), func(e *workflowModel.FileInfo) (string, *workflowModel.FileInfo) {
return e.FileURL, e
})
var wfOpts []compose.WorkflowOption
wfOpts = append(wfOpts, compose.WithIDAsName(wfEntity.ID))
if s := execute.GetStaticConfig(); s != nil && s.MaxNodeCountPerWorkflow > 0 {
wfOpts = append(wfOpts, compose.WithMaxNodeCount(s.MaxNodeCountPerWorkflow))
@ -468,6 +520,8 @@ func (i *impl) StreamExecute(ctx context.Context, config workflowModel.ExecuteCo
config.CommitID = wfEntity.CommitID
var cOpts []nodes.ConvertOption
inputFileFields := make(map[string]*workflowModel.FileInfo)
cOpts = append(cOpts, nodes.WithCollectFileFields(inputFileFields), nodes.WithNotNeedTrimQueryFileName(true))
if config.InputFailFast {
cOpts = append(cOpts, nodes.FailFast())
}
@ -478,6 +532,9 @@ func (i *impl) StreamExecute(ctx context.Context, config workflowModel.ExecuteCo
} else if ws != nil {
logs.CtxWarnf(ctx, "convert inputs warnings: %v", *ws)
}
for k, v := range inputFileFields {
config.InputFileFields[k] = v
}
inStr, err := sonic.MarshalString(input)
if err != nil {
@ -997,20 +1054,6 @@ func (i *impl) checkApplicationWorkflowReleaseVersion(ctx context.Context, appID
return nil
}
const maxHistoryRounds int64 = 30
func (i *impl) calculateMaxChatHistoryRounds(ctx context.Context, wfEntity *entity.Workflow, repo workflow.Repository) (int64, error) {
if wfEntity == nil {
return 0, nil
}
maxRounds, err := getMaxHistoryRoundsRecursively(ctx, wfEntity, repo)
if err != nil {
return 0, err
}
return min(maxRounds, maxHistoryRounds), nil
}
func (i *impl) prefetchChatHistory(ctx context.Context, config workflowModel.ExecuteConfig, historyRounds int64) ([]*crossmessage.WfMessage, []*schema.Message, error) {
convID := config.ConversationID
agentID := config.AgentID
@ -1027,11 +1070,11 @@ func (i *impl) prefetchChatHistory(ctx context.Context, config workflowModel.Exe
return nil, nil, nil
}
var resolvedAppID int64
var bizID int64
if appID != nil {
resolvedAppID = *appID
bizID = *appID
} else if agentID != nil {
resolvedAppID = *agentID
bizID = *agentID
} else {
logs.CtxWarnf(ctx, "AppID and AgentID are both nil, skipping chat history")
return nil, nil, nil
@ -1039,7 +1082,7 @@ func (i *impl) prefetchChatHistory(ctx context.Context, config workflowModel.Exe
runIdsReq := &crossmessage.GetLatestRunIDsRequest{
ConversationID: *convID,
AppID: resolvedAppID,
BizID: bizID,
UserID: userID,
Rounds: historyRounds + 1,
SectionID: *sectionID,
@ -1048,7 +1091,7 @@ func (i *impl) prefetchChatHistory(ctx context.Context, config workflowModel.Exe
runIds, err := crossmessage.DefaultSVC().GetLatestRunIDs(ctx, runIdsReq)
if err != nil {
logs.CtxErrorf(ctx, "failed to get latest run ids: %v", err)
return nil, nil, nil
return nil, nil, err
}
if len(runIds) <= 1 {
return []*crossmessage.WfMessage{}, []*schema.Message{}, nil
@ -1061,7 +1104,7 @@ func (i *impl) prefetchChatHistory(ctx context.Context, config workflowModel.Exe
})
if err != nil {
logs.CtxErrorf(ctx, "failed to get messages by run ids: %v", err)
return nil, nil, nil
return nil, nil, err
}
return response.Messages, response.SchemaMessages, nil

View File

@ -0,0 +1,286 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package service
import (
"context"
"errors"
"testing"
"github.com/cloudwego/eino/schema"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
workflowModel "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/workflow"
crossmessage "github.com/coze-dev/coze-studio/backend/crossdomain/contract/message"
messagemock "github.com/coze-dev/coze-studio/backend/crossdomain/contract/message/messagemock"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
mock_workflow "github.com/coze-dev/coze-studio/backend/internal/mock/domain/workflow"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
)
func TestImpl_handleHistory(t *testing.T) {
ctx := context.Background()
ctrl := gomock.NewController(t, gomock.WithOverridableExpectations())
defer ctrl.Finish()
// Setup for cross-domain service mock
mockMessage := messagemock.NewMockMessage(ctrl)
crossmessage.SetDefaultSVC(mockMessage)
tests := []struct {
name string
setupMock func(service *mock_workflow.MockService, msgSvc *messagemock.MockMessage, repo *mock_workflow.MockRepository)
config *workflowModel.ExecuteConfig
input map[string]any
historyRounds int64
shouldFetch bool
expectErr bool
expectedHistory []*crossmessage.WfMessage
expectedSchemaHistory []*schema.Message
}{
{
name: "historyRounds is zero",
historyRounds: 0,
shouldFetch: true,
config: &workflowModel.ExecuteConfig{},
setupMock: func(service *mock_workflow.MockService, msgSvc *messagemock.MockMessage, repo *mock_workflow.MockRepository) {
},
expectErr: false,
},
{
name: "shouldFetch is false",
historyRounds: 5,
shouldFetch: false,
config: &workflowModel.ExecuteConfig{
AppID: ptr.Of(int64(1)),
ConversationID: ptr.Of(int64(100)),
SectionID: ptr.Of(int64(101)),
},
setupMock: func(service *mock_workflow.MockService, msgSvc *messagemock.MockMessage, repo *mock_workflow.MockRepository) {
msgSvc.EXPECT().GetLatestRunIDs(gomock.Any(), gomock.Any()).Return([]int64{1, 2}, nil).AnyTimes()
msgSvc.EXPECT().GetMessagesByRunIDs(gomock.Any(), gomock.Any()).Return(&crossmessage.GetMessagesByRunIDsResponse{
Messages: []*crossmessage.WfMessage{{ID: 1}},
SchemaMessages: []*schema.Message{{
Role: schema.User,
Content: "123",
}},
}, nil).AnyTimes()
},
expectErr: false,
expectedHistory: []*crossmessage.WfMessage{{ID: 1}},
expectedSchemaHistory: []*schema.Message{{
Role: schema.User,
Content: "123",
}},
},
{
name: "fetch conversation by name - conversation exists",
historyRounds: 3,
shouldFetch: true,
config: &workflowModel.ExecuteConfig{AppID: ptr.Of(int64(1))},
input: map[string]any{"CONVERSATION_NAME": "test-conv"},
setupMock: func(service *mock_workflow.MockService, msgSvc *messagemock.MockMessage, repo *mock_workflow.MockRepository) {
service.EXPECT().GetOrCreateConversation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), "test-conv").Return(int64(200), int64(201), nil).AnyTimes()
msgSvc.EXPECT().GetLatestRunIDs(gomock.Any(), gomock.Any()).Return([]int64{3, 4}, nil).AnyTimes()
msgSvc.EXPECT().GetMessagesByRunIDs(gomock.Any(), gomock.Any()).Return(&crossmessage.GetMessagesByRunIDsResponse{
Messages: []*crossmessage.WfMessage{{ID: 2}},
SchemaMessages: []*schema.Message{{
Role: schema.Assistant,
Content: "123",
}},
}, nil).AnyTimes()
repo.EXPECT().GetConversationTemplate(gomock.Any(), gomock.Any(), gomock.Any()).Return(&entity.ConversationTemplate{
TemplateID: int64(202),
SpaceID: int64(203),
AppID: int64(204),
}, true, nil).AnyTimes()
repo.EXPECT().GetOrCreateStaticConversation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(int64(205), int64(206), true, nil).AnyTimes()
},
expectErr: false,
expectedHistory: []*crossmessage.WfMessage{{ID: 2}},
expectedSchemaHistory: []*schema.Message{{
Role: schema.Assistant,
Content: "123",
}},
},
{
name: "fetch conversation by name - conversation not exists",
historyRounds: 3,
shouldFetch: true,
config: &workflowModel.ExecuteConfig{AgentID: ptr.Of(int64(2))},
input: map[string]any{"CONVERSATION_NAME": "new-conv"},
setupMock: func(service *mock_workflow.MockService, msgSvc *messagemock.MockMessage, repo *mock_workflow.MockRepository) {
service.EXPECT().GetOrCreateConversation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), "new-conv").Return(int64(300), int64(301), nil).AnyTimes()
msgSvc.EXPECT().GetLatestRunIDs(gomock.Any(), gomock.Any()).Return([]int64{5, 6}, nil).AnyTimes()
msgSvc.EXPECT().GetMessagesByRunIDs(gomock.Any(), gomock.Any()).Return(&crossmessage.GetMessagesByRunIDsResponse{
Messages: []*crossmessage.WfMessage{{ID: 3}},
}, nil).AnyTimes()
repo.EXPECT().GetConversationTemplate(gomock.Any(), gomock.Any(), gomock.Any()).Return(&entity.ConversationTemplate{
TemplateID: int64(202),
SpaceID: int64(203),
AppID: int64(204),
}, false, nil).AnyTimes()
repo.EXPECT().GetOrCreateDynamicConversation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(int64(205), int64(206), true, nil).AnyTimes()
},
expectErr: false,
expectedHistory: []*crossmessage.WfMessage{{ID: 3}},
},
{
name: "input with wrong type for conversation name",
historyRounds: 5,
shouldFetch: true,
config: &workflowModel.ExecuteConfig{AppID: ptr.Of(int64(1))},
input: map[string]any{"CONVERSATION_NAME": 12345},
setupMock: func(service *mock_workflow.MockService, msgSvc *messagemock.MockMessage, repo *mock_workflow.MockRepository) {
},
expectErr: true,
},
{
name: "GetOrCreateConversation returns error",
historyRounds: 5,
shouldFetch: true,
config: &workflowModel.ExecuteConfig{AppID: ptr.Of(int64(1))},
input: map[string]any{"CONVERSATION_NAME": "fail-conv"},
setupMock: func(service *mock_workflow.MockService, msgSvc *messagemock.MockMessage, repo *mock_workflow.MockRepository) {
service.EXPECT().GetOrCreateConversation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), "fail-conv").Return(int64(0), int64(0), errors.New("db error")).AnyTimes()
repo.EXPECT().GetConversationTemplate(gomock.Any(), gomock.Any(), gomock.Any()).Return(&entity.ConversationTemplate{
TemplateID: int64(202),
SpaceID: int64(203),
AppID: int64(204),
}, false, nil).AnyTimes()
repo.EXPECT().GetOrCreateDynamicConversation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(int64(205), int64(206), true, errors.New("db error")).AnyTimes()
},
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockService := mock_workflow.NewMockService(ctrl)
mockRepo := mock_workflow.NewMockRepository(ctrl)
testImpl := &impl{repo: mockRepo, conversationImpl: &conversationImpl{repo: mockRepo}}
tt.setupMock(mockService, mockMessage, mockRepo)
err := testImpl.handleHistory(ctx, tt.config, tt.input, tt.historyRounds, tt.shouldFetch)
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
if tt.expectedHistory != nil {
assert.Equal(t, tt.expectedHistory, tt.config.ConversationHistory)
} else if tt.historyRounds == 0 {
assert.Nil(t, tt.config.ConversationHistory)
} else if tt.expectedSchemaHistory != nil {
assert.Equal(t, tt.expectedSchemaHistory, tt.config.ConversationHistorySchemaMessages)
}
}
})
}
}
func TestImpl_prefetchChatHistory(t *testing.T) {
ctx := context.Background()
ctrl := gomock.NewController(t, gomock.WithOverridableExpectations())
defer ctrl.Finish()
mockMessage := messagemock.NewMockMessage(ctrl)
crossmessage.SetDefaultSVC(mockMessage)
tests := []struct {
name string
setupMock func(msgSvc *messagemock.MockMessage)
config workflowModel.ExecuteConfig
historyRounds int64
expectErr bool
}{
{
name: "SectionID is nil",
config: workflowModel.ExecuteConfig{
ConversationID: ptr.Of(int64(100)),
AppID: ptr.Of(int64(1)),
},
historyRounds: 5,
setupMock: func(msgSvc *messagemock.MockMessage) {},
expectErr: false,
},
{
name: "ConversationID is nil",
config: workflowModel.ExecuteConfig{
SectionID: ptr.Of(int64(101)),
AppID: ptr.Of(int64(1)),
},
historyRounds: 5,
setupMock: func(msgSvc *messagemock.MockMessage) {},
expectErr: false,
},
{
name: "AppID and AgentID are both nil",
config: workflowModel.ExecuteConfig{
ConversationID: ptr.Of(int64(100)),
SectionID: ptr.Of(int64(101)),
},
historyRounds: 5,
setupMock: func(msgSvc *messagemock.MockMessage) {},
expectErr: false,
},
{
name: "GetLatestRunIDs returns error",
config: workflowModel.ExecuteConfig{
AppID: ptr.Of(int64(1)),
ConversationID: ptr.Of(int64(100)),
SectionID: ptr.Of(int64(101)),
},
historyRounds: 5,
setupMock: func(msgSvc *messagemock.MockMessage) {
msgSvc.EXPECT().GetLatestRunIDs(gomock.Any(), gomock.Any()).Return(nil, errors.New("db error"))
},
expectErr: true,
},
{
name: "GetMessagesByRunIDs returns error",
config: workflowModel.ExecuteConfig{
AppID: ptr.Of(int64(1)),
ConversationID: ptr.Of(int64(100)),
SectionID: ptr.Of(int64(101)),
},
historyRounds: 5,
setupMock: func(msgSvc *messagemock.MockMessage) {
msgSvc.EXPECT().GetLatestRunIDs(gomock.Any(), gomock.Any()).Return([]int64{1, 2, 3}, nil)
msgSvc.EXPECT().GetMessagesByRunIDs(gomock.Any(), gomock.Any()).Return(nil, errors.New("db error"))
},
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testImpl := &impl{}
tt.setupMock(mockMessage)
_, _, err := testImpl.prefetchChatHistory(ctx, tt.config, tt.historyRounds)
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@ -39,7 +39,6 @@ import (
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/canvas/adaptor"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/canvas/convert"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/nodes"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/repo"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/schema"
"github.com/coze-dev/coze-studio/backend/infra/contract/cache"
@ -522,7 +521,7 @@ func isEnableChatHistory(s *schema.NodeSchema) bool {
return false
}
chatHistoryAware, ok := s.Configs.(nodes.ChatHistoryAware)
chatHistoryAware, ok := s.Configs.(schema.ChatHistoryAware)
if !ok {
return false
}
@ -2171,15 +2170,15 @@ func (i *impl) adaptToChatFlow(ctx context.Context, wID int64) error {
vMap[v.Name] = true
}
if _, ok := vMap["USER_INPUT"]; !ok {
if _, ok := vMap[vo.UserInputKey]; !ok {
startNode.Data.Outputs = append(startNode.Data.Outputs, &vo.Variable{
Name: "USER_INPUT",
Name: vo.UserInputKey,
Type: vo.VariableTypeString,
})
}
if _, ok := vMap["CONVERSATION_NAME"]; !ok {
if _, ok := vMap[vo.ConversationNameKey]; !ok {
startNode.Data.Outputs = append(startNode.Data.Outputs, &vo.Variable{
Name: "CONVERSATION_NAME",
Name: vo.ConversationNameKey,
Type: vo.VariableTypeString,
DefaultValue: "Default",
})

View File

@ -22,15 +22,11 @@ import (
"strconv"
"strings"
workflowModel "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/workflow"
"github.com/coze-dev/coze-studio/backend/api/model/workflow"
wf "github.com/coze-dev/coze-studio/backend/domain/workflow"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/canvas/adaptor"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/canvas/validate"
"github.com/coze-dev/coze-studio/backend/domain/workflow/variable"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ternary"
"github.com/coze-dev/coze-studio/backend/pkg/sonic"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
@ -201,214 +197,3 @@ func isIncremental(prev version, next version) bool {
return next.Patch > prev.Patch
}
func getMaxHistoryRoundsRecursively(ctx context.Context, wfEntity *entity.Workflow, repo wf.Repository) (int64, error) {
visited := make(map[string]struct{})
maxRounds := int64(0)
err := getMaxHistoryRoundsRecursiveHelper(ctx, wfEntity, repo, visited, &maxRounds)
return maxRounds, err
}
func getMaxHistoryRoundsRecursiveHelper(ctx context.Context, wfEntity *entity.Workflow, repo wf.Repository, visited map[string]struct{}, maxRounds *int64) error {
visitedKey := fmt.Sprintf("%d:%s", wfEntity.ID, wfEntity.GetVersion())
if _, ok := visited[visitedKey]; ok {
return nil
}
visited[visitedKey] = struct{}{}
var canvas vo.Canvas
if err := sonic.UnmarshalString(wfEntity.Canvas, &canvas); err != nil {
return fmt.Errorf("failed to unmarshal canvas for workflow %d: %w", wfEntity.ID, err)
}
return collectMaxHistoryRounds(ctx, canvas.Nodes, repo, visited, maxRounds)
}
func collectMaxHistoryRounds(ctx context.Context, nodes []*vo.Node, repo wf.Repository, visited map[string]struct{}, maxRounds *int64) error {
for _, node := range nodes {
if node == nil {
continue
}
if node.Data != nil && node.Data.Inputs != nil && node.Data.Inputs.ChatHistorySetting != nil && node.Data.Inputs.ChatHistorySetting.EnableChatHistory {
if node.Data.Inputs.ChatHistorySetting.ChatHistoryRound > *maxRounds {
*maxRounds = node.Data.Inputs.ChatHistorySetting.ChatHistoryRound
}
} else if node.Type == entity.NodeTypeLLM.IDStr() && node.Data != nil && node.Data.Inputs != nil && node.Data.Inputs.LLMParam != nil {
param := node.Data.Inputs.LLMParam
bs, _ := sonic.Marshal(param)
llmParam := make(vo.LLMParam, 0)
if err := sonic.Unmarshal(bs, &llmParam); err != nil {
return err
}
var chatHistoryEnabled bool
var chatHistoryRound int64
for _, param := range llmParam {
switch param.Name {
case "enableChatHistory":
if val, ok := param.Input.Value.Content.(bool); ok {
b := val
chatHistoryEnabled = b
}
case "chatHistoryRound":
if strVal, ok := param.Input.Value.Content.(string); ok {
int64Val, err := strconv.ParseInt(strVal, 10, 64)
if err != nil {
return err
}
chatHistoryRound = int64Val
}
}
}
if chatHistoryEnabled {
if chatHistoryRound > *maxRounds {
*maxRounds = chatHistoryRound
}
}
}
isSubWorkflow := node.Type == entity.NodeTypeSubWorkflow.IDStr() && node.Data != nil && node.Data.Inputs != nil
if isSubWorkflow {
workflowIDStr := node.Data.Inputs.WorkflowID
if workflowIDStr == "" {
continue
}
workflowID, err := strconv.ParseInt(workflowIDStr, 10, 64)
if err != nil {
return fmt.Errorf("invalid workflow ID in sub-workflow node %s: %w", node.ID, err)
}
subWfEntity, err := repo.GetEntity(ctx, &vo.GetPolicy{
ID: workflowID,
QType: ternary.IFElse(len(node.Data.Inputs.WorkflowVersion) == 0, workflowModel.FromDraft, workflowModel.FromSpecificVersion),
Version: node.Data.Inputs.WorkflowVersion,
})
if err != nil {
return fmt.Errorf("failed to get sub-workflow entity %d: %w", workflowID, err)
}
if err := getMaxHistoryRoundsRecursiveHelper(ctx, subWfEntity, repo, visited, maxRounds); err != nil {
return err
}
}
if len(node.Blocks) > 0 {
if err := collectMaxHistoryRounds(ctx, node.Blocks, repo, visited, maxRounds); err != nil {
return err
}
}
}
return nil
}
func getHistoryRoundsFromNode(ctx context.Context, wfEntity *entity.Workflow, nodeID string, repo wf.Repository) (int64, error) {
if wfEntity == nil {
return 0, nil
}
visited := make(map[string]struct{})
visitedKey := fmt.Sprintf("%d:%s", wfEntity.ID, wfEntity.GetVersion())
if _, ok := visited[visitedKey]; ok {
return 0, nil
}
visited[visitedKey] = struct{}{}
maxRounds := int64(0)
c := &vo.Canvas{}
if err := sonic.UnmarshalString(wfEntity.Canvas, c); err != nil {
return 0, fmt.Errorf("failed to unmarshal canvas: %w", err)
}
var (
n *vo.Node
nodeFinder func(nodes []*vo.Node) *vo.Node
)
nodeFinder = func(nodes []*vo.Node) *vo.Node {
for i := range nodes {
if nodes[i].ID == nodeID {
return nodes[i]
}
if len(nodes[i].Blocks) > 0 {
if n := nodeFinder(nodes[i].Blocks); n != nil {
return n
}
}
}
return nil
}
n = nodeFinder(c.Nodes)
if n.Type == entity.NodeTypeLLM.IDStr() {
if n.Data == nil || n.Data.Inputs == nil {
return 0, nil
}
param := n.Data.Inputs.LLMParam
bs, _ := sonic.Marshal(param)
llmParam := make(vo.LLMParam, 0)
if err := sonic.Unmarshal(bs, &llmParam); err != nil {
return 0, err
}
var chatHistoryEnabled bool
var chatHistoryRound int64
for _, param := range llmParam {
switch param.Name {
case "enableChatHistory":
if val, ok := param.Input.Value.Content.(bool); ok {
b := val
chatHistoryEnabled = b
}
case "chatHistoryRound":
if strVal, ok := param.Input.Value.Content.(string); ok {
int64Val, err := strconv.ParseInt(strVal, 10, 64)
if err != nil {
return 0, err
}
chatHistoryRound = int64Val
}
}
}
if chatHistoryEnabled {
return chatHistoryRound, nil
}
return 0, nil
}
if n.Type == entity.NodeTypeIntentDetector.IDStr() || n.Type == entity.NodeTypeKnowledgeRetriever.IDStr() {
if n.Data != nil && n.Data.Inputs != nil && n.Data.Inputs.ChatHistorySetting != nil && n.Data.Inputs.ChatHistorySetting.EnableChatHistory {
return n.Data.Inputs.ChatHistorySetting.ChatHistoryRound, nil
}
return 0, nil
}
if n.Type == entity.NodeTypeSubWorkflow.IDStr() {
if n.Data != nil && n.Data.Inputs != nil {
workflowIDStr := n.Data.Inputs.WorkflowID
if workflowIDStr == "" {
return 0, nil
}
workflowID, err := strconv.ParseInt(workflowIDStr, 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid workflow ID in sub-workflow node %s: %w", n.ID, err)
}
subWfEntity, err := repo.GetEntity(ctx, &vo.GetPolicy{
ID: workflowID,
QType: ternary.IFElse(len(n.Data.Inputs.WorkflowVersion) == 0, workflowModel.FromDraft, workflowModel.FromSpecificVersion),
Version: n.Data.Inputs.WorkflowVersion,
})
if err != nil {
return 0, fmt.Errorf("failed to get sub-workflow entity %d: %w", workflowID, err)
}
if err := getMaxHistoryRoundsRecursiveHelper(ctx, subWfEntity, repo, visited, &maxRounds); err != nil {
return 0, err
}
return maxRounds, nil
}
}
if len(n.Blocks) > 0 {
if err := collectMaxHistoryRounds(ctx, n.Blocks, repo, visited, &maxRounds); err != nil {
return 0, err
}
}
return maxRounds, nil
}

View File

@ -11,8 +11,8 @@ require (
github.com/apache/rocketmq-client-go/v2 v2.1.3-0.20250427084711-67ec50b93040
github.com/apache/thrift v0.21.0
github.com/aws/aws-sdk-go-v2 v1.36.6
github.com/aws/aws-sdk-go-v2/config v1.29.1
github.com/aws/aws-sdk-go-v2/credentials v1.17.54
github.com/aws/aws-sdk-go-v2/config v1.29.9
github.com/aws/aws-sdk-go-v2/credentials v1.17.62
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.1
github.com/bytedance/gopkg v0.1.3
github.com/bytedance/mockey v1.2.14
@ -51,7 +51,7 @@ require (
github.com/nikolalohinski/gonja v1.5.3 // indirect
github.com/nsqio/go-nsq v1.1.0
github.com/ollama/ollama v0.9.6
github.com/onsi/gomega v1.27.3
github.com/onsi/gomega v1.35.1
github.com/pingcap/tidb/pkg/parser v0.0.0-20250417044355-c5882b1f6c58
github.com/pkg/errors v0.9.1
github.com/rbretecher/go-postman-collection v0.9.0
@ -71,7 +71,7 @@ require (
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
golang.org/x/image v0.22.0
golang.org/x/mod v0.25.0
golang.org/x/oauth2 v0.25.0
golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.16.0
google.golang.org/genai v1.18.0
gopkg.in/yaml.v2 v2.4.0
@ -84,26 +84,26 @@ require (
)
require (
cloud.google.com/go v0.116.0 // indirect
cloud.google.com/go/auth v0.9.3 // indirect
cloud.google.com/go v0.118.3 // indirect
cloud.google.com/go/auth v0.15.0 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect
github.com/anthropics/anthropic-sdk-go v1.4.0 // indirect
github.com/avast/retry-go v3.0.0+incompatible // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.37 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.18 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect
github.com/aws/smithy-go v1.22.4 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
@ -146,15 +146,14 @@ require (
github.com/goccy/go-json v0.10.5 // indirect
github.com/godbus/dbus/v5 v5.0.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.7.0-rc.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.5 // indirect
github.com/goph/emperror v0.17.2 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
@ -187,7 +186,6 @@ require (
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-sqlite3 v1.14.15 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/meguminnnnnnnnn/go-openai v0.0.0-20250620092828-0d508a1dcdde // indirect
github.com/milvus-io/milvus/pkg/v2 v2.0.0-20250319085209-5a6b4e56d59e // indirect
github.com/minio/crc64nvme v1.0.1 // indirect
@ -207,10 +205,10 @@ require (
github.com/pingcap/log v1.1.1-0.20221015072633-39906604fb81 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
@ -219,7 +217,7 @@ require (
github.com/rs/xid v1.6.0 // indirect
github.com/samber/lo v1.27.0 // indirect
github.com/shabbyrobe/xmlwriter v0.0.0-20200208144257-9fca06d00ffa // indirect
github.com/shirou/gopsutil/v3 v3.22.9 // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f
github.com/smarty/assertions v1.16.0 // indirect
@ -249,9 +247,8 @@ require (
go.etcd.io/etcd/pkg/v3 v3.5.5 // indirect
go.etcd.io/etcd/raft/v3 v3.5.5 // indirect
go.etcd.io/etcd/server/v3 v3.5.5 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 // indirect
@ -270,8 +267,8 @@ require (
golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.33.0 // indirect
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
@ -280,15 +277,46 @@ require (
gorm.io/datatypes v1.1.1-0.20230130040222-c43177d3cf8c // indirect
gorm.io/driver/postgres v1.5.11 // indirect
gorm.io/hints v1.1.0 // indirect
k8s.io/apimachinery v0.28.6 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
k8s.io/apimachinery v0.32.3 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
stathat.com/c/consistent v1.0.0 // indirect
)
require github.com/apache/pulsar-client-go v0.16.0
require (
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/99designs/keyring v1.2.1 // indirect
github.com/AthenZ/athenz v1.12.13 // indirect
github.com/DataDog/zstd v1.5.0 // indirect
github.com/ardielle/ardielle-go v1.5.2 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/bits-and-blooms/bitset v1.4.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
github.com/dvsekhvalnov/jose2go v1.6.0 // indirect
github.com/eino-contrib/jsonschema v1.0.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/hamba/avro/v2 v2.26.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
golang.org/x/term v0.32.0 // indirect
k8s.io/client-go v0.32.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
)

View File

@ -13,10 +13,10 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U=
cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk=
cloud.google.com/go v0.118.3 h1:jsypSnrE/w4mJysioGdMBg4MiW/hHx/sArFpaBWHdME=
cloud.google.com/go v0.118.3/go.mod h1:Lhs3YLnBlwJ4KA6nuObNMZ/fCbOQBPuWKPoE0Wa/9Vc=
cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@ -37,10 +37,20 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o=
github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA=
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/AthenZ/athenz v1.12.13 h1:OhZNqZsoBXNrKBJobeUUEirPDnwt0HRo4kQMIO1UwwQ=
github.com/AthenZ/athenz v1.12.13/go.mod h1:XXDXXgaQzXaBXnJX6x/bH4yF6eon2lkyzQZ0z/dxprE=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@ -49,12 +59,16 @@ github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMd
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/zstd v1.5.0 h1:+K/VEwIAaPcHiMtQvpLD4lqW7f0Gk3xdYZmI1hD+CXo=
github.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/IBM/sarama v1.45.1 h1:nY30XqYpqyXOXSNoe2XCgjj9jklGM1Ye94ierUb1jQ0=
github.com/IBM/sarama v1.45.1/go.mod h1:qifDhA3VWSrQ1TjSMyxDl3nYL3oX2C83u+G6L79sq4w=
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
@ -78,10 +92,14 @@ github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqR
github.com/anthropics/anthropic-sdk-go v1.4.0 h1:fU1jKxYbQdQDiEXCxeW5XZRIOwKevn/PMg8Ay1nnUx0=
github.com/anthropics/anthropic-sdk-go v1.4.0/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apache/pulsar-client-go v0.16.0 h1:SnmGzqcTu6WpK4D6I2Jdwe/VCFkMUk516OiIF3DHqI8=
github.com/apache/pulsar-client-go v0.16.0/go.mod h1:ow9PhLoGUY6ncrKOtjnWeJycFnTKOwrIV39j3kNV54M=
github.com/apache/rocketmq-client-go/v2 v2.1.3-0.20250427084711-67ec50b93040 h1:c2o4/foDm9LXc3jSmm3SUxVZb5I5KNtztw/bstf836s=
github.com/apache/rocketmq-client-go/v2 v2.1.3-0.20250427084711-67ec50b93040/go.mod h1:6I6vgxHR3hzrvn+6n/4mrhS+UTulzK/X9LB2Vk1U5gE=
github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/ardielle/ardielle-go v1.5.2 h1:TilHTpHIQJ27R1Tl/iITBzMwiUGSlVfiVhwDNGM3Zj4=
github.com/ardielle/ardielle-go v1.5.2/go.mod h1:I4hy1n795cUhaVt/ojz83SNVCYIGsAFAONtv2Dr7HUI=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@ -96,18 +114,18 @@ github.com/aws/aws-sdk-go-v2 v1.36.6 h1:zJqGjVbRdTPojeCGWn5IR5pbJwSQSBh5RWFTQcEQ
github.com/aws/aws-sdk-go-v2 v1.36.6/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
github.com/aws/aws-sdk-go-v2/config v1.29.1 h1:JZhGawAyZ/EuJeBtbQYnaoftczcb2drR2Iq36Wgz4sQ=
github.com/aws/aws-sdk-go-v2/config v1.29.1/go.mod h1:7bR2YD5euaxBhzt2y/oDkt3uNRb6tjFp98GlTFueRwk=
github.com/aws/aws-sdk-go-v2/credentials v1.17.54 h1:4UmqeOqJPvdvASZWrKlhzpRahAulBfyTJQUaYy4+hEI=
github.com/aws/aws-sdk-go-v2/credentials v1.17.54/go.mod h1:RTdfo0P0hbbTxIhmQrOsC/PquBZGabEPnCaxxKRPSnI=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 h1:5grmdTdMsovn9kPZPI23Hhvp0ZyNm5cRO+IZFIYiAfw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24/go.mod h1:zqi7TVKTswH3Ozq28PkmBmgzG1tona7mo9G2IJg4Cis=
github.com/aws/aws-sdk-go-v2/config v1.29.9 h1:Kg+fAYNaJeGXp1vmjtidss8O2uXIsXwaRqsQJKXVr+0=
github.com/aws/aws-sdk-go-v2/config v1.29.9/go.mod h1:oU3jj2O53kgOU4TXq/yipt6ryiooYjlkqqVaZk7gY/U=
github.com/aws/aws-sdk-go-v2/credentials v1.17.62 h1:fvtQY3zFzYJ9CfixuAQ96IxDrBajbBWGqjNTCa79ocU=
github.com/aws/aws-sdk-go-v2/credentials v1.17.62/go.mod h1:ElETBxIQqcxej++Cs8GyPBbgMys5DgQPTwo7cUPDKt8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 h1:osMWfm/sC/L4tvEdQ65Gri5ZZDCUpuYJZbTTDrsn4I0=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37/go.mod h1:ZV2/1fbjOPr4G4v38G3Ww5TBT4+hmsK45s/rxu1fGy0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 h1:v+X21AvTb2wZ+ycg1gx+orkB/9U6L7AOp93R7qYxsxM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37/go.mod h1:G0uM1kyssELxmJ2VZEfG0q2npObR3BAkF3c1VsfVnfs=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.37 h1:XTZZ0I3SZUHAtBLBU6395ad+VOblE0DwQP6MuaNeics=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.37/go.mod h1:Pi6ksbniAWVwu2S8pEzcYPyhUkAcLaufxN7PfAUQjBk=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
@ -121,12 +139,12 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.18 h1:OS2e0SKqsU2Li
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.18/go.mod h1:+Yrk+MDGzlNGxCXieljNeWpoZTCQUQVL+Jk9hGGJ8qM=
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.1 h1:RkHXU9jP0DptGy7qKI8CBGsUJruWz0v5IgwBa2DwWcU=
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.1/go.mod h1:3xAOf7tdKF+qbb+XpU+EPhNXAdun3Lu1RcDrj8KC24I=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 h1:kuIyu4fTT38Kj7YCC7ouNbVZSSpqkZ+LzIfhCr6Dg+I=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.11/go.mod h1:Ro744S4fKiCCuZECXgOi760TiYylUM8ZBf6OGiZzJtY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 h1:l+dgv/64iVlQ3WsBbnn+JSbkj01jIi+SM0wYsj3y/hY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10/go.mod h1:Fzsj6lZEb8AkTE5S68OhcbBqeWPsR8RnGuKPr8Todl8=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 h1:BRVDbewN6VZcwr+FBOszDKvYeXY1kJ+GGMCcpghlw0U=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.9/go.mod h1:f6vjfZER1M17Fokn0IzssOTMT2N8ZSq+7jnNF0tArvw=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 h1:KwuLovgQPcdjNMfFt9OhUd9a2OwcOKhxfvF4glTzLuA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
@ -140,6 +158,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bits-and-blooms/bitset v1.4.0 h1:+YZ8ePm+He2pU3dZlIZiOeAKfrBkXi1lSrXJ/Xzgbu8=
github.com/bits-and-blooms/bitset v1.4.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
@ -249,6 +269,12 @@ github.com/cohesion-org/deepseek-go v1.3.2 h1:WTZ/2346KFYca+n+DL5p+Ar1RQxF2w/wGk
github.com/cohesion-org/deepseek-go v1.3.2/go.mod h1:bOVyKj38r90UEYZFrmJOzJKPxuAh8sIzHOCnLOpiXeI=
github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0=
github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0=
github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII=
github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
@ -260,11 +286,15 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@ -277,11 +307,21 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/dimfeld/httptreemux v5.0.1+incompatible h1:Qj3gVcDNoOthBAqftuD596rm4wg/adLLz5xh5CmpiCA=
github.com/dimfeld/httptreemux v5.0.1+incompatible/go.mod h1:rbUlSV+CCpv/SuqUTP/8Bk2O3LyUV436/yaRGkhP6Z0=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY=
github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=
@ -324,6 +364,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c=
github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
@ -339,6 +381,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/getkin/kin-openapi v0.118.0 h1:z43njxPmJ7TaPpMSCQb7PN0dEYno4tyBPQcrFdHoLuM=
github.com/getkin/kin-openapi v0.118.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc=
@ -359,6 +403,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs=
@ -389,9 +435,10 @@ github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
@ -400,6 +447,8 @@ github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
@ -414,6 +463,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
@ -426,7 +477,6 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@ -481,6 +531,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
@ -501,16 +552,20 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro=
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
github.com/googleapis/enterprise-certificate-proxy v0.3.5 h1:VgzTY2jogw3xt39CusEnFJWm7rlsq5yL5q9XdLOuP5g=
github.com/googleapis/enterprise-certificate-proxy v0.3.5/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18=
github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@ -535,6 +590,10 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
github.com/hamba/avro/v2 v2.26.0 h1:IaT5l6W3zh7K67sMrT2+RreJyDTllBGVJm4+Hedk9qE=
github.com/hamba/avro/v2 v2.26.0/go.mod h1:I8glyswHnpED3Nlx2ZdUe+4LJnCOOyiCzLMno9i/Uu0=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
@ -692,12 +751,16 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
@ -730,8 +793,6 @@ github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOj
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
github.com/meguminnnnnnnnn/go-openai v0.0.0-20250620092828-0d508a1dcdde h1:pq2I0uxUR4lfr4OmqvE8QdHj9UML9b1jZu8L3dI2eu8=
github.com/meguminnnnnnnnn/go-openai v0.0.0-20250620092828-0d508a1dcdde/go.mod h1:CqSFsV6AkkL2fixd25WYjRAolns+gQrY1x/Cz9c30v8=
@ -767,6 +828,20 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -776,7 +851,13 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
@ -795,6 +876,7 @@ github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTf
github.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE=
github.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg=
github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U=
@ -811,8 +893,8 @@ github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vv
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE=
github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
@ -820,9 +902,13 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/onsi/gomega v1.27.3 h1:5VwIwnBY3vbBDOJrNtA4rVdiTZCsq9B5F12pvy1Drmk=
github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0=
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
@ -881,14 +967,14 @@ github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3O
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
@ -896,8 +982,8 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@ -905,8 +991,8 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0=
github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I=
@ -945,8 +1031,12 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shabbyrobe/xmlwriter v0.0.0-20200208144257-9fca06d00ffa h1:2cO3RojjYl3hVTbEvJVqrMaFmORhL6O06qdW42toftk=
github.com/shabbyrobe/xmlwriter v0.0.0-20200208144257-9fca06d00ffa/go.mod h1:Yjr3bdWaVWyME1kha7X0jsz3k2DgXNa1Pj3XGyUAbx8=
github.com/shirou/gopsutil/v3 v3.22.9 h1:yibtJhIVEMcdw+tCTbOPiF1VcsuDeTE4utJ8Dm4c5eA=
github.com/shirou/gopsutil/v3 v3.22.9/go.mod h1:bBYl1kjgEJpWpxeHmLI+dVHWtyAwfcmSBLDsp2TNT8A=
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
@ -1011,6 +1101,8 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tealeg/xlsx/v3 v3.3.13 h1:Zk1Stj11MGRnOYI1st6av/Z2lIXp/jFZomrSWSeJLmY=
github.com/tealeg/xlsx/v3 v3.3.13/go.mod h1:KV4FTFtvGy0TBlOivJLZu/YNZk6e0Qtk7eOSglWksuA=
github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo=
github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4=
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@ -1025,10 +1117,8 @@ 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/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@ -1099,7 +1189,7 @@ github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@ -1129,13 +1219,13 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.25.0/go.mod h1:E5NNboN0UqSAki0Atn9kVwaN7I+l25gGxDqBueo/74E=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
go.opentelemetry.io/otel v1.0.1/go.mod h1:OPEOD4jIT2SlZPMmwT6FqZz2C0ZNdQqiWcoK6M0SNFU=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
@ -1323,8 +1413,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -1406,13 +1496,13 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -1422,6 +1512,7 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
@ -1587,10 +1678,10 @@ google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU=
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE=
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf h1:dHDlF3CWxQkefK9IJx+O8ldY0gLygvrlYRBNbPqDWuY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
@ -1638,6 +1729,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
@ -1654,6 +1746,7 @@ gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
@ -1703,15 +1796,25 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/apimachinery v0.28.6 h1:RsTeR4z6S07srPg6XYrwXpTJVMXsjPXn0ODakMytSW0=
k8s.io/apimachinery v0.28.6/go.mod h1:QFNX/kCl/EMT2WTSz8k4WLCv2XnkOLMaL8GAVRMdpsA=
k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro=
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
stathat.com/c/consistent v1.0.0 h1:ezyc51EGcRPJUxfHGSgJjWzJdj3NiMU9pNfLNGiXV0c=
stathat.com/c/consistent v1.0.0/go.mod h1:QkzMWzcbB+yQBL2AttO6sgsQS/JSTapcDISJalmCDS0=

View File

@ -18,10 +18,15 @@ package storage
import (
"context"
"errors"
"io"
"time"
)
var (
ErrObjectNotFound = errors.New("object not found")
)
//go:generate mockgen -destination ../../../internal/mock/infra/contract/storage/storage_mock.go -package mock -source storage.go Factory
type Storage interface {
// PutObject puts the object with the specified key.
@ -68,10 +73,10 @@ type ListObjectsPaginatedOutput struct {
}
type FileInfo struct {
Key string
LastModified time.Time
ETag string
Size int64
URL string
Tagging map[string]string
Key string `json:"key"`
LastModified time.Time `json:"last_modified"`
ETag string `json:"etag"`
Size int64 `json:"size"`
URL string `json:"url"`
Tagging map[string]string `json:"tagging"`
}

View File

@ -74,20 +74,23 @@ func (r *runner) Run(ctx context.Context, request *coderunner.RunRequest) (*code
}
func (r *runner) pythonCmdRun(_ context.Context, code string, params map[string]any) (map[string]any, error) {
bs, _ := sonic.Marshal(params)
bs, err := sonic.Marshal(params)
if err != nil {
return nil, fmt.Errorf("failed to marshal params to json, err: %w", err)
}
cmd := exec.Command(goutil.GetPython3Path(), "-c", fmt.Sprintf(pythonCode, code), string(bs)) // ignore_security_alert RCE
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
cmd.Stdout = stdout
cmd.Stderr = stderr
err := cmd.Run()
err = cmd.Run()
if err != nil {
return nil, fmt.Errorf("failed to run python script err: %s, std err: %s", err.Error(), stderr.String())
}
if stderr.String() != "" {
return nil, fmt.Errorf("failed to run python script err: %s", stderr.String())
}
ret := make(map[string]any)
err = sonic.Unmarshal(stdout.Bytes(), &ret)
if err != nil {

View File

@ -21,6 +21,7 @@ import (
"encoding/base64"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
@ -47,6 +48,7 @@ func ParseMarkdown(config *contract.Config, storage storage.Storage, ocr ocr.OCR
return nil, err
}
node := mdParser.Parse(text.NewReader(b))
cs := config.ChunkingStrategy
ps := config.ParsingStrategy
@ -101,13 +103,118 @@ func ParseMarkdown(config *contract.Config, storage storage.Storage, ocr ocr.OCR
return text
}
// validateImageURL 验证图片URL的安全性
validateImageURL := func(urlString string) error {
parsedURL, err := url.Parse(urlString)
if err != nil {
return err
}
// 只允许HTTP/HTTPS
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return fmt.Errorf("unsupported scheme: %s", parsedURL.Scheme)
}
// 检查域名白名单
allowedDomains := []string{
"images.unsplash.com",
"cdn.example.com",
"github.com",
"githubusercontent.com",
// 可以根据需要添加其他受信任的域名
}
hostname := parsedURL.Hostname()
for _, domain := range allowedDomains {
if hostname == domain || strings.HasSuffix(hostname, "."+domain) {
return nil
}
}
return fmt.Errorf("domain not allowed: %s", hostname)
}
// isPrivateIPAddress 检查IP地址是否为私有地址
isPrivateIPAddress := func(ip net.IP) bool {
// 检查私有IP范围
privateRanges := []struct {
cidr string
}{
{"10.0.0.0/8"},
{"172.16.0.0/12"},
{"192.168.0.0/16"},
{"127.0.0.0/8"},
{"169.254.0.0/16"}, // 链路本地地址
{"::1/128"}, // IPv6 loopback
{"fc00::/7"}, // IPv6 私有地址
}
for _, r := range privateRanges {
_, cidr, _ := net.ParseCIDR(r.cidr)
if cidr.Contains(ip) {
return true
}
}
return false
}
// isPrivateIP 检查是否为私有IP地址
isPrivateIP := func(host string) bool {
ip := net.ParseIP(host)
if ip == nil {
// 可能是域名,需要解析
ips, err := net.LookupIP(host)
if err != nil {
return true // 解析失败,拒绝访问
}
// 检查所有解析的IP
for _, resolvedIP := range ips {
if isPrivateIPAddress(resolvedIP) {
return true
}
}
return false
}
return isPrivateIPAddress(ip)
}
downloadImage := func(ctx context.Context, url string) ([]byte, error) {
client := &http.Client{Timeout: 5 * time.Second}
// URL验证
if err := validateImageURL(url); err != nil {
return nil, fmt.Errorf("invalid URL: %w", err)
}
// 使用安全的HTTP客户端
client := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
// 禁止访问私有IP
host, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
if isPrivateIP(host) {
return nil, fmt.Errorf("access to private IP denied: %s", host)
}
return (&net.Dialer{}).DialContext(ctx, network, addr)
},
},
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
}
// 添加安全头
req.Header.Set("User-Agent", "CozeStudio/1.0")
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to download image: %w", err)
@ -118,7 +225,11 @@ func ParseMarkdown(config *contract.Config, storage storage.Storage, ocr ocr.OCR
return nil, fmt.Errorf("failed to download image, status code: %d", resp.StatusCode)
}
data, err := io.ReadAll(resp.Body)
// 限制响应大小
const maxImageSize = 10 * 1024 * 1024 // 10MB
limitedReader := io.LimitReader(resp.Body, maxImageSize)
data, err := io.ReadAll(limitedReader)
if err != nil {
return nil, fmt.Errorf("failed to read image content: %w", err)
}

View File

@ -21,12 +21,12 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/coze-dev/coze-studio/backend/pkg/parsex"
"io"
"os"
"github.com/elastic/go-elasticsearch/v7"
"github.com/elastic/go-elasticsearch/v7/esapi"
"github.com/elastic/go-elasticsearch/v7/esutil"
"io"
"os"
"github.com/coze-dev/coze-studio/backend/infra/contract/es"
"github.com/coze-dev/coze-studio/backend/pkg/lang/conv"
@ -39,7 +39,7 @@ type es7Client struct {
}
func newES7() (Client, error) {
addresses, err := parsex.ParseClusterEndpoints(os.Getenv("ES_ADDR"))
addresses, err := parseClusterEndpoints(os.Getenv("ES_ADDR"))
if err != nil {
return nil, err
}
@ -123,8 +123,8 @@ func (c *es7Client) CreateIndex(ctx context.Context, index string, properties ma
"properties": properties,
},
"settings": map[string]any{
"number_of_shards": parsex.GetEnvDefaultIntSetting("ES_NUMBER_OF_SHARDS", "1"),
"number_of_replicas": parsex.GetEnvDefaultIntSetting("ES_NUMBER_OF_REPLICAS", "1"),
"number_of_shards": getEnvDefaultIntSetting("ES_NUMBER_OF_SHARDS", "1"),
"number_of_replicas": getEnvDefaultIntSetting("ES_NUMBER_OF_REPLICAS", "1"),
},
}

View File

@ -19,7 +19,8 @@ package es
import (
"context"
"fmt"
"github.com/coze-dev/coze-studio/backend/pkg/parsex"
"os"
"github.com/elastic/go-elasticsearch/v8"
"github.com/elastic/go-elasticsearch/v8/esutil"
"github.com/elastic/go-elasticsearch/v8/typedapi/core/search"
@ -30,7 +31,6 @@ import (
"github.com/elastic/go-elasticsearch/v8/typedapi/types/enums/operator"
"github.com/elastic/go-elasticsearch/v8/typedapi/types/enums/sortorder"
"github.com/elastic/go-elasticsearch/v8/typedapi/types/enums/textquerytype"
"os"
"github.com/coze-dev/coze-studio/backend/infra/contract/es"
"github.com/coze-dev/coze-studio/backend/pkg/lang/conv"
@ -51,7 +51,7 @@ type es8BulkIndexer struct {
type es8Types struct{}
func newES8() (Client, error) {
addresses, err := parsex.ParseClusterEndpoints(os.Getenv("ES_ADDR"))
addresses, err := parseClusterEndpoints(os.Getenv("ES_ADDR"))
if err != nil {
return nil, err
}
@ -243,8 +243,8 @@ func (c *es8Client) CreateIndex(ctx context.Context, index string, properties ma
Properties: propertiesMap,
},
Settings: &types.IndexSettings{
NumberOfShards: parsex.GetEnvDefaultIntSetting("ES_NUMBER_OF_SHARDS", "1"),
NumberOfReplicas: parsex.GetEnvDefaultIntSetting("ES_NUMBER_OF_REPLICAS", "1"),
NumberOfShards: getEnvDefaultIntSetting("ES_NUMBER_OF_SHARDS", "1"),
NumberOfReplicas: getEnvDefaultIntSetting("ES_NUMBER_OF_REPLICAS", "1"),
},
}).Do(ctx); err != nil {
return err

View File

@ -18,8 +18,9 @@ package es
import (
"fmt"
"github.com/coze-dev/coze-studio/backend/infra/contract/es"
"os"
"github.com/coze-dev/coze-studio/backend/infra/contract/es"
)
type (

View File

@ -14,18 +14,18 @@
* limitations under the License.
*/
package parsex
package es
import (
"fmt"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
"os"
"strconv"
"strings"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
)
// ParseClusterEndpoints 解析 ES /kafka 地址,多个地址用逗号分隔
func ParseClusterEndpoints(address string) ([]string, error) {
func parseClusterEndpoints(address string) ([]string, error) {
if strings.TrimSpace(address) == "" {
return nil, fmt.Errorf("endpoints environment variable is required")
}
@ -52,8 +52,7 @@ func ParseClusterEndpoints(address string) ([]string, error) {
return validEndpoints, nil
}
// GetEnvDefaultIntSetting 获取环境变量的值,如果不存在或无效则返回默认值
func GetEnvDefaultIntSetting(envVar, defaultValue string) string {
func getEnvDefaultIntSetting(envVar, defaultValue string) string {
value := os.Getenv(envVar)
if value == "" {
return defaultValue

View File

@ -23,6 +23,7 @@ import (
"github.com/coze-dev/coze-studio/backend/infra/contract/eventbus"
"github.com/coze-dev/coze-studio/backend/infra/impl/eventbus/kafka"
"github.com/coze-dev/coze-studio/backend/infra/impl/eventbus/nsq"
"github.com/coze-dev/coze-studio/backend/infra/impl/eventbus/pulsar"
"github.com/coze-dev/coze-studio/backend/infra/impl/eventbus/rmq"
"github.com/coze-dev/coze-studio/backend/types/consts"
)
@ -54,9 +55,11 @@ func (consumerServiceImpl) RegisterConsumer(nameServer, topic, group string, con
return kafka.RegisterConsumer(nameServer, topic, group, consumerHandler, opts...)
case "rmq":
return rmq.RegisterConsumer(nameServer, topic, group, consumerHandler, opts...)
case "pulsar":
return pulsar.RegisterConsumer(nameServer, topic, group, consumerHandler, opts...)
}
return fmt.Errorf("invalid mq type: %s , only support nsq, kafka, rmq", tp)
return fmt.Errorf("invalid mq type: %s , only support nsq, kafka, rmq, pulsar", tp)
}
func NewProducer(nameServer, topic, group string, retries int) (eventbus.Producer, error) {
@ -68,7 +71,9 @@ func NewProducer(nameServer, topic, group string, retries int) (eventbus.Produce
return kafka.NewProducer(nameServer, topic)
case "rmq":
return rmq.NewProducer(nameServer, topic, group, retries)
case "pulsar":
return pulsar.NewProducer(nameServer, topic, group)
}
return nil, fmt.Errorf("invalid mq type: %s , only support nsq, kafka, rmq", tp)
return nil, fmt.Errorf("invalid mq type: %s , only support nsq, kafka, rmq, pulsar", tp)
}

View File

@ -0,0 +1,141 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package pulsar
import (
"context"
"fmt"
"os"
"github.com/apache/pulsar-client-go/pulsar"
"github.com/coze-dev/coze-studio/backend/infra/contract/eventbus"
"github.com/coze-dev/coze-studio/backend/pkg/lang/signal"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
"github.com/coze-dev/coze-studio/backend/pkg/safego"
"github.com/coze-dev/coze-studio/backend/types/consts"
)
func RegisterConsumer(serviceURL, topic, group string, consumerHandler eventbus.ConsumerHandler, opts ...eventbus.ConsumerOpt) error {
if serviceURL == "" {
return fmt.Errorf("service URL is empty")
}
if topic == "" {
return fmt.Errorf("topic is empty")
}
if group == "" {
return fmt.Errorf("group is empty")
}
if consumerHandler == nil {
return fmt.Errorf("consumer handler is nil")
}
// Parse consumer options
option := &eventbus.ConsumerOption{}
for _, opt := range opts {
opt(option)
}
// Prepare client options
clientOptions := pulsar.ClientOptions{
URL: serviceURL,
}
// Add JWT authentication if token is provided
if jwtToken := os.Getenv(consts.PulsarJWTToken); jwtToken != "" {
clientOptions.Authentication = pulsar.NewAuthenticationToken(jwtToken)
}
// Create Pulsar client
client, err := pulsar.NewClient(clientOptions)
if err != nil {
return fmt.Errorf("create pulsar client failed: %w", err)
}
// Configure consumer options
consumerOptions := pulsar.ConsumerOptions{
Topic: topic,
SubscriptionName: group,
Type: pulsar.Exclusive, // Exclusive mode ensures single consumer for message ordering
}
// Create consumer
consumer, err := client.Subscribe(consumerOptions)
if err != nil {
client.Close()
return fmt.Errorf("create pulsar consumer failed: %w", err)
}
// Create cancellable context for better resource management
ctx, cancel := context.WithCancel(context.Background())
// Start consuming messages in a goroutine
safego.Go(ctx, func() {
defer func() {
consumer.Close()
client.Close()
}()
for {
select {
case <-ctx.Done():
logs.Infof("pulsar consumer stopped for topic: %s, group: %s", topic, group)
return
default:
// Receive message with context timeout
msg, err := consumer.Receive(ctx)
if err != nil {
// Check if context was cancelled
if ctx.Err() != nil {
return
}
logs.Errorf("receive pulsar message error: %v", err)
continue
}
// Convert to eventbus message
eventMsg := &eventbus.Message{
Topic: topic,
Group: group,
Body: msg.Payload(),
}
// Handle message with context
if err := consumerHandler.HandleMessage(ctx, eventMsg); err != nil {
logs.Errorf("handle pulsar message failed, topic: %s, group: %s, err: %v", topic, group, err)
// Negative acknowledge on error
consumer.Nack(msg)
continue
}
// Acknowledge message on success
consumer.Ack(msg)
}
}
})
// Handle graceful shutdown
safego.Go(context.Background(), func() {
signal.WaitExit()
logs.Infof("shutting down pulsar consumer for topic: %s, group: %s", topic, group)
cancel() // Cancel the context to stop consumer loop
consumer.Close()
client.Close()
})
return nil
}

View File

@ -0,0 +1,155 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package pulsar
import (
"context"
"fmt"
"os"
"github.com/apache/pulsar-client-go/pulsar"
"github.com/coze-dev/coze-studio/backend/infra/contract/eventbus"
"github.com/coze-dev/coze-studio/backend/pkg/lang/signal"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
"github.com/coze-dev/coze-studio/backend/pkg/safego"
"github.com/coze-dev/coze-studio/backend/types/consts"
)
type producerImpl struct {
topic string
client pulsar.Client
producer pulsar.Producer
}
func NewProducer(serviceURL, topic, group string) (eventbus.Producer, error) {
if serviceURL == "" {
return nil, fmt.Errorf("pulsar service URL is required")
}
if topic == "" {
return nil, fmt.Errorf("topic is required")
}
// Prepare client options
clientOptions := pulsar.ClientOptions{
URL: serviceURL,
}
// Add JWT authentication if token is provided
if jwtToken := os.Getenv(consts.PulsarJWTToken); jwtToken != "" {
clientOptions.Authentication = pulsar.NewAuthenticationToken(jwtToken)
}
// Create Pulsar client
logs.Debugf("Creating Pulsar client with URL: %s", serviceURL)
if jwtToken := os.Getenv(consts.PulsarJWTToken); jwtToken != "" {
logs.Debugf("Using JWT authentication, token length: %d", len(jwtToken))
} else {
logs.Debugf("No JWT token provided")
}
client, err := pulsar.NewClient(clientOptions)
if err != nil {
return nil, fmt.Errorf("failed to create pulsar client with URL %s: %w", serviceURL, err)
}
logs.Debugf("Pulsar client created successfully")
// Create producer
logs.Debugf("Creating producer for topic: %s, group: %s", topic, group)
producer, err := client.CreateProducer(pulsar.ProducerOptions{
Topic: topic,
Name: fmt.Sprintf("%s-producer", group),
})
if err != nil {
logs.Errorf("Failed to create producer: %v", err)
client.Close()
return nil, fmt.Errorf("create pulsar producer failed: %w", err)
}
logs.Debugf("Producer created successfully")
impl := &producerImpl{
topic: topic,
client: client,
producer: producer,
}
// Handle graceful shutdown
safego.Go(context.Background(), func() {
signal.WaitExit()
impl.close()
})
return impl, nil
}
func (p *producerImpl) Send(ctx context.Context, body []byte, opts ...eventbus.SendOpt) error {
return p.BatchSend(ctx, [][]byte{body}, opts...)
}
func (p *producerImpl) BatchSend(ctx context.Context, bodyArr [][]byte, opts ...eventbus.SendOpt) error {
option := eventbus.SendOption{}
for _, opt := range opts {
opt(&option)
}
// Use Pulsar's async send with batch collection for better performance
type sendResult struct {
err error
}
resultChan := make(chan sendResult, len(bodyArr))
pendingCount := len(bodyArr)
for _, body := range bodyArr {
msg := &pulsar.ProducerMessage{
Payload: body,
}
// Set partition key if sharding key is provided
if option.ShardingKey != nil {
msg.Key = *option.ShardingKey
}
// Send message asynchronously for better batching performance
p.producer.SendAsync(ctx, msg, func(messageID pulsar.MessageID, producerMessage *pulsar.ProducerMessage, err error) {
resultChan <- sendResult{err: err}
})
}
// Wait for all messages to be sent
for i := 0; i < pendingCount; i++ {
select {
case result := <-resultChan:
if result.err != nil {
return fmt.Errorf("[pulsarProducer] batch send message failed: %w", result.err)
}
case <-ctx.Done():
return fmt.Errorf("[pulsarProducer] batch send cancelled: %w", ctx.Err())
}
}
return nil
}
func (p *producerImpl) close() {
if p.producer != nil {
p.producer.Close()
}
if p.client != nil {
p.client.Close()
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package pulsar
import (
"context"
"os"
"sync"
"testing"
"time"
"github.com/apache/pulsar-client-go/pulsar"
"github.com/stretchr/testify/assert"
"github.com/coze-dev/coze-studio/backend/types/consts"
)
var serviceURL = "pulsar://localhost:6650"
func TestProducer(t *testing.T) {
if os.Getenv("PULSAR_LOCAL_TEST") != "true" {
return
}
// JWT token should be set via environment variable PULSAR_JWT_TOKEN if needed
// For local testing, you can set: export PULSAR_JWT_TOKEN="your-jwt-token"
clientOptions := pulsar.ClientOptions{
URL: serviceURL,
}
if jwtToken := os.Getenv(consts.PulsarJWTToken); jwtToken != "" {
clientOptions.Authentication = pulsar.NewAuthenticationToken(jwtToken)
}
client, err := pulsar.NewClient(clientOptions)
assert.NoError(t, err)
defer client.Close()
producer, err := client.CreateProducer(pulsar.ProducerOptions{
Topic: "test_topic",
Name: "test_group_producer",
})
assert.NoError(t, err)
defer producer.Close()
msgID, err := producer.Send(context.Background(), &pulsar.ProducerMessage{
Payload: []byte("hello"),
})
assert.NoError(t, err)
t.Logf("Message sent with ID: %v", msgID)
}
func TestConsumer(t *testing.T) {
if os.Getenv("PULSAR_LOCAL_TEST") != "true" {
return
}
// JWT token should be set via environment variable PULSAR_JWT_TOKEN if needed
// For local testing, you can set: export PULSAR_JWT_TOKEN="your-jwt-token"
clientOptions := pulsar.ClientOptions{
URL: serviceURL,
}
if jwtToken := os.Getenv(consts.PulsarJWTToken); jwtToken != "" {
clientOptions.Authentication = pulsar.NewAuthenticationToken(jwtToken)
}
client, err := pulsar.NewClient(clientOptions)
assert.NoError(t, err)
defer client.Close()
// First create consumer
consumer, err := client.Subscribe(pulsar.ConsumerOptions{
Topic: "test_topic",
SubscriptionName: "test_group_consumer",
Type: pulsar.Shared,
})
assert.NoError(t, err)
defer consumer.Close()
// Then create producer and send a message
producer, err := client.CreateProducer(pulsar.ProducerOptions{
Topic: "test_topic",
Name: "test_consumer_producer",
})
assert.NoError(t, err)
defer producer.Close()
// Send a test message
_, err = producer.Send(context.Background(), &pulsar.ProducerMessage{
Payload: []byte("consumer test message"),
})
assert.NoError(t, err)
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
msg, err := consumer.Receive(context.Background())
if err != nil {
t.Errorf("Failed to receive message: %v", err)
return
}
t.Logf("Received message: %s", string(msg.Payload()))
consumer.Ack(msg)
}()
wg.Wait()
time.Sleep(time.Second)
}

View File

@ -64,15 +64,16 @@ func (c *OceanBaseClient) BatchInsertVectors(ctx context.Context, collectionName
}
func (c *OceanBaseClient) DeleteVector(ctx context.Context, collectionName string, vectorID string) error {
return c.official.GetDB().WithContext(ctx).Exec("DELETE FROM "+collectionName+" WHERE vector_id = ?", vectorID).Error
return c.official.GetDB().WithContext(ctx).Table(collectionName).Where("vector_id = ?", vectorID).Delete(nil).Error
}
func (c *OceanBaseClient) InitDatabase(ctx context.Context) error {
return c.official.GetDB().WithContext(ctx).Exec("SELECT 1").Error
var result int
return c.official.GetDB().WithContext(ctx).Raw("SELECT 1").Scan(&result).Error
}
func (c *OceanBaseClient) DropCollection(ctx context.Context, collectionName string) error {
return c.official.GetDB().WithContext(ctx).Exec("DROP TABLE IF EXISTS " + collectionName).Error
return c.official.GetDB().WithContext(ctx).Migrator().DropTable(collectionName)
}
type SearchStrategy interface {

View File

@ -43,6 +43,15 @@ type VectorResult struct {
CreatedAt time.Time `json:"created_at"`
}
type VectorRecord struct {
VectorID string `gorm:"column:vector_id;primaryKey"`
Content string `gorm:"column:content;type:text;not null"`
Metadata string `gorm:"column:metadata;type:json"`
Embedding string `gorm:"column:embedding;type:vector;not null"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;default:CURRENT_TIMESTAMP"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"`
}
type CollectionInfo struct {
Name string `json:"name"`
Dimension int `json:"dimension"`
@ -83,21 +92,23 @@ func (c *OceanBaseOfficialClient) setVectorParameters() error {
}
func (c *OceanBaseOfficialClient) CreateCollection(ctx context.Context, collectionName string, dimension int) error {
createTableSQL := fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (
vector_id VARCHAR(255) PRIMARY KEY,
content TEXT NOT NULL,
metadata JSON,
embedding VECTOR(%d) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_created_at (created_at),
INDEX idx_content (content(100))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`, collectionName, dimension)
if !c.db.WithContext(ctx).Migrator().HasTable(collectionName) {
createTableSQL := fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (
vector_id VARCHAR(255) PRIMARY KEY,
content TEXT NOT NULL,
metadata JSON,
embedding VECTOR(%d) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_created_at (created_at),
INDEX idx_content (content(100))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`, collectionName, dimension)
if err := c.db.WithContext(ctx).Exec(createTableSQL).Error; err != nil {
return fmt.Errorf("failed to create table: %v", err)
if err := c.db.WithContext(ctx).Exec(createTableSQL).Error; err != nil {
return fmt.Errorf("failed to create table: %v", err)
}
}
createIndexSQL := fmt.Sprintf(`
@ -136,30 +147,19 @@ func (c *OceanBaseOfficialClient) InsertVectors(ctx context.Context, collectionN
}
func (c *OceanBaseOfficialClient) insertBatch(ctx context.Context, collectionName string, batch []VectorResult) error {
placeholders := make([]string, len(batch))
values := make([]interface{}, 0, len(batch)*5)
for j, vector := range batch {
placeholders[j] = "(?, ?, ?, ?, NOW())"
values = append(values,
vector.VectorID,
vector.Content,
vector.Metadata,
c.vectorToString(vector.Embedding),
)
records := make([]VectorRecord, len(batch))
for i, vector := range batch {
records[i] = VectorRecord{
VectorID: vector.VectorID,
Content: vector.Content,
Metadata: vector.Metadata,
Embedding: c.vectorToString(vector.Embedding),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}
sql := fmt.Sprintf(`
INSERT INTO %s (vector_id, content, metadata, embedding, created_at)
VALUES %s
ON DUPLICATE KEY UPDATE
content = VALUES(content),
metadata = VALUES(metadata),
embedding = VALUES(embedding),
updated_at = NOW()
`, collectionName, strings.Join(placeholders, ","))
return c.db.WithContext(ctx).Exec(sql, values...).Error
return c.db.WithContext(ctx).Table(collectionName).Save(&records).Error
}
func (c *OceanBaseOfficialClient) SearchVectors(
@ -341,24 +341,28 @@ func (c *OceanBaseOfficialClient) DebugCollectionData(ctx context.Context, colle
log.Printf("[Debug] Collection '%s' exists with %d vectors", collectionName, count)
log.Printf("[Debug] Sample data from collection '%s':", collectionName)
rows, err := c.db.WithContext(ctx).Raw(`
SELECT vector_id, content, created_at
FROM ` + collectionName + `
ORDER BY created_at DESC
LIMIT 5
`).Rows()
var samples []struct {
VectorID string `gorm:"column:vector_id"`
Content string `gorm:"column:content"`
CreatedAt time.Time `gorm:"column:created_at"`
}
err := c.db.WithContext(ctx).Table(collectionName).
Select("vector_id, content, created_at").
Order("created_at DESC").
Limit(5).
Find(&samples).Error
if err != nil {
log.Printf("[Debug] Failed to get sample data: %v", err)
} else {
defer rows.Close()
for rows.Next() {
var vectorID, content string
var createdAt time.Time
if err := rows.Scan(&vectorID, &content, &createdAt); err != nil {
log.Printf("[Debug] Failed to scan sample row: %v", err)
continue
for _, sample := range samples {
contentPreview := sample.Content
if len(contentPreview) > 50 {
contentPreview = contentPreview[:50]
}
log.Printf("[Debug] Sample: ID=%s, Content=%s, Created=%s", vectorID, content[:min(50, len(content))], createdAt)
log.Printf("[Debug] Sample: ID=%s, Content=%s, Created=%s",
sample.VectorID, contentPreview, sample.CreatedAt)
}
}

View File

@ -646,15 +646,15 @@ func (m *mysqlService) ExecuteSQL(ctx context.Context, req *rdb.ExecuteSQLReques
var processedParams []interface{}
var err error
// Handle SQLType: if raw, do not process params
// 禁用原始SQL执行以防止SQL注入攻击
if req.SQLType == entity2.SQLType_Raw {
processedSQL = req.SQL
processedParams = nil
} else {
processedSQL, processedParams, err = m.processSliceParams(req.SQL, req.Params)
if err != nil {
return nil, fmt.Errorf("failed to process parameters: %v", err)
}
return nil, fmt.Errorf("raw SQL execution is not allowed for security reasons")
}
// 强制使用参数化查询
processedSQL, processedParams, err = m.processSliceParams(req.SQL, req.Params)
if err != nil {
return nil, fmt.Errorf("failed to process parameters: %v", err)
}
operation, err := sqlparser.NewSQLParser().GetSQLOperation(processedSQL)
@ -1011,4 +1011,4 @@ func (m *mysqlService) buildNestedConditions(condition *rdb.ComplexCondition) (s
return whereClause.String(), values, nil
}
return "", values, nil
}
}

View File

@ -30,7 +30,7 @@ func AssembleFileUrl(ctx context.Context, urlExpire *int64, files []*storage.Fil
taskGroup := taskgroup.NewTaskGroup(ctx, 5)
for idx := range files {
f := files[idx]
expire := int64(60 * 60 * 24)
expire := int64(7 * 60 * 60 * 24)
if urlExpire != nil && *urlExpire > 0 {
expire = *urlExpire
}

View File

@ -1,63 +0,0 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package proxy
import (
"context"
"net"
"net/url"
"os"
"github.com/coze-dev/coze-studio/backend/pkg/ctxcache"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
"github.com/coze-dev/coze-studio/backend/types/consts"
)
func CheckIfNeedReplaceHost(ctx context.Context, originURLStr string) (ok bool, proxyURL string) {
// url parse
originURL, err := url.Parse(originURLStr)
if err != nil {
logs.CtxWarnf(ctx, "[CheckIfNeedReplaceHost] url parse failed, err: %v", err)
return false, ""
}
proxyPort := os.Getenv(consts.MinIOProxyEndpoint) // :8889
if proxyPort == "" {
return false, ""
}
currentHost, ok := ctxcache.Get[string](ctx, consts.HostKeyInCtx)
if !ok {
return false, ""
}
currentScheme, ok := ctxcache.Get[string](ctx, consts.RequestSchemeKeyInCtx)
if !ok {
return false, ""
}
host, _, err := net.SplitHostPort(currentHost)
if err != nil {
host = currentHost
}
minioProxyHost := host + proxyPort
originURL.Host = minioProxyHost
originURL.Scheme = currentScheme
logs.CtxDebugf(ctx, "[CheckIfNeedReplaceHost] reset originURL.String = %s", originURL.String())
return true, originURL.String()
}

View File

@ -30,7 +30,6 @@ import (
"github.com/coze-dev/coze-studio/backend/infra/contract/storage"
"github.com/coze-dev/coze-studio/backend/infra/impl/storage/internal/fileutil"
"github.com/coze-dev/coze-studio/backend/infra/impl/storage/internal/proxy"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
)
@ -232,12 +231,6 @@ func (m *minioClient) GetObjectUrl(ctx context.Context, objectKey string, opts .
return "", fmt.Errorf("GetObjectUrl failed: %v", err)
}
// logs.CtxDebugf(ctx, "[GetObjectUrl] origin presignedURL.String = %s", presignedURL.String())
ok, proxyURL := proxy.CheckIfNeedReplaceHost(ctx, presignedURL.String())
if ok {
return proxyURL, nil
}
return presignedURL.String(), nil
}
@ -317,7 +310,7 @@ func (m *minioClient) HeadObject(ctx context.Context, objectKey string, opts ...
stat, err := m.client.StatObject(ctx, m.bucketName, objectKey, minio.StatObjectOptions{})
if err != nil {
if minio.ToErrorResponse(err).Code == "NoSuchKey" {
return nil, nil
return nil, storage.ErrObjectNotFound
}
return nil, fmt.Errorf("HeadObject failed for key %s: %w", objectKey, err)

View File

@ -32,7 +32,6 @@ import (
"github.com/coze-dev/coze-studio/backend/infra/contract/storage"
"github.com/coze-dev/coze-studio/backend/infra/impl/storage/internal/fileutil"
"github.com/coze-dev/coze-studio/backend/infra/impl/storage/internal/proxy"
"github.com/coze-dev/coze-studio/backend/pkg/goutil"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
"github.com/coze-dev/coze-studio/backend/pkg/taskgroup"
@ -230,21 +229,26 @@ func (t *s3Client) GetObjectUrl(ctx context.Context, objectKey string, opts ...s
bucket := t.bucketName
presignClient := s3.NewPresignClient(client)
opt := storage.GetOption{}
for _, optFn := range opts {
optFn(&opt)
}
expire := int64(60 * 60 * 24)
if opt.Expire > 0 {
expire = opt.Expire
}
req, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(objectKey),
}, func(options *s3.PresignOptions) {
options.Expires = time.Duration(60*60*24) * time.Second
options.Expires = time.Duration(expire) * time.Second
})
if err != nil {
return "", fmt.Errorf("get object presigned url failed: %v", err)
}
ok, proxyURL := proxy.CheckIfNeedReplaceHost(ctx, req.URL)
if ok {
return proxyURL, nil
}
return req.URL, nil
}
@ -381,7 +385,7 @@ func (t *s3Client) HeadObject(ctx context.Context, objectKey string, opts ...sto
if err != nil {
var nsk *types.NotFound
if errors.As(err, &nsk) {
return nil, nil
return nil, storage.ErrObjectNotFound
}
return nil, err
}

View File

@ -30,7 +30,6 @@ import (
"github.com/coze-dev/coze-studio/backend/infra/contract/storage"
"github.com/coze-dev/coze-studio/backend/infra/impl/storage/internal/fileutil"
"github.com/coze-dev/coze-studio/backend/infra/impl/storage/internal/proxy"
"github.com/coze-dev/coze-studio/backend/pkg/goutil"
"github.com/coze-dev/coze-studio/backend/pkg/lang/conv"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
@ -247,9 +246,19 @@ func (t *tosClient) GetObjectUrl(ctx context.Context, objectKey string, opts ...
client := t.client
bucketName := t.bucketName
opt := storage.GetOption{}
for _, optFn := range opts {
optFn(&opt)
}
expire := int64(7 * 24 * 60 * 60)
if opt.Expire > 0 {
expire = opt.Expire
}
output, err := client.PreSignedURL(&tos.PreSignedURLInput{
HTTPMethod: enum.HttpMethodGet,
Expires: 60 * 60 * 24,
Expires: expire,
Bucket: bucketName,
Key: objectKey,
})
@ -257,11 +266,6 @@ func (t *tosClient) GetObjectUrl(ctx context.Context, objectKey string, opts ...
return "", err
}
ok, proxyURL := proxy.CheckIfNeedReplaceHost(ctx, output.SignedUrl)
if ok {
return proxyURL, nil
}
return output.SignedUrl, nil
}
@ -389,7 +393,7 @@ func (t *tosClient) HeadObject(ctx context.Context, objectKey string, opts ...st
if err != nil {
if serverErr, ok := err.(*tos.TosServerError); ok {
if serverErr.StatusCode == http.StatusNotFound {
return nil, nil
return nil, storage.ErrObjectNotFound
}
}
return nil, err

View File

@ -22,10 +22,6 @@ import (
"context"
"crypto/tls"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"runtime/debug"
"strings"
@ -41,7 +37,6 @@ import (
"github.com/coze-dev/coze-studio/backend/pkg/lang/conv"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ternary"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
"github.com/coze-dev/coze-studio/backend/pkg/safego"
"github.com/coze-dev/coze-studio/backend/types/consts"
)
@ -60,7 +55,6 @@ func main() {
panic("InitializeInfra failed, err=" + err.Error())
}
asyncStartMinioProxyServer(ctx)
startHttpServer()
}
@ -160,56 +154,3 @@ func setCrashOutput() {
crashFile, _ := os.Create("crash.log")
debug.SetCrashOutput(crashFile, debug.CrashOptions{})
}
// TODO: remove me later
func asyncStartMinioProxyServer(ctx context.Context) {
storageType := getEnv(consts.StorageType, "minio")
proxyURL := getEnv(consts.MinIOAPIHost, "http://localhost:9000")
if storageType == "tos" {
proxyURL = getEnv(consts.TOSBucketEndpoint, "https://opencoze.tos-cn-beijing.volces.com")
}
if storageType == "s3" {
proxyURL = getEnv(consts.S3BucketEndpoint, "")
}
minioProxyEndpoint := getEnv(consts.MinIOProxyEndpoint, "")
if len(minioProxyEndpoint) == 0 {
return
}
safego.Go(ctx, func() {
target, err := url.Parse(proxyURL)
if err != nil {
log.Fatal(err)
}
proxy := httputil.NewSingleHostReverseProxy(target)
originDirector := proxy.Director
proxy.Director = func(req *http.Request) {
q := req.URL.Query()
q.Del("x-wf-file_name")
req.URL.RawQuery = q.Encode()
originDirector(req)
req.Host = req.URL.Host
}
useSSL := getEnv(consts.UseSSL, "0")
if useSSL == "1" {
logs.Infof("Minio proxy server is listening on %s with SSL", minioProxyEndpoint)
err := http.ListenAndServeTLS(minioProxyEndpoint,
getEnv(consts.SSLCertFile, ""),
getEnv(consts.SSLKeyFile, ""), proxy)
if err != nil {
log.Fatal(err)
}
} else {
logs.Infof("Minio proxy server is listening on %s", minioProxyEndpoint)
err := http.ListenAndServe(minioProxyEndpoint, proxy)
if err != nil {
log.Fatal(err)
}
}
})
}

View File

@ -59,6 +59,8 @@ const (
MQServer = "MQ_NAME_SERVER"
RMQSecretKey = "RMQ_SECRET_KEY"
RMQAccessKey = "RMQ_ACCESS_KEY"
PulsarServiceURL = "PULSAR_SERVICE_URL"
PulsarJWTToken = "PULSAR_JWT_TOKEN"
RMQTopicApp = "opencoze_search_app"
RMQTopicResource = "opencoze_search_resource"
RMQTopicKnowledge = "opencoze_knowledge"

View File

@ -74,12 +74,17 @@ export ES_PASSWORD=""
export ES_NUMBER_OF_SHARDS = "1"
export ES_NUMBER_OF_REPLICAS = "1"
export COZE_MQ_TYPE="nsq" # nsq / kafka / rmq
# Backend Event Bus
export COZE_MQ_TYPE="nsq" # nsq / kafka / rmq / pulsar
export MQ_NAME_SERVER="nsqd:4150"
# RocketMQ
export RMQ_ACCESS_KEY=""
export RMQ_SECRET_KEY=""
# Pulsar
# Use Pulsar as backend eventbus, MQ_NAME_SERVER example is: "pulsar:6650"
# Fill PULSAR_JWT_TOKEN for JWT auth, leave empty for no auth
export PULSAR_SERVICE_URL="pulsar://pulsar-service:6650"
export PULSAR_JWT_TOKEN=""
# Settings for VectorStore
# VectorStore type: milvus / vikingdb / oceanbase

View File

@ -20,7 +20,7 @@ services:
- ./data/mysql:/var/lib/mysql
- ./volumes/mysql/schema.sql:/docker-entrypoint-initdb.d/init.sql
- ./atlas/opencoze_latest_schema.hcl:/opencoze_latest_schema.hcl:ro
entrypoint:
entrypoint:
- bash
- -c
- |
@ -43,7 +43,7 @@ services:
sleep 2
fi
done
echo 'MySQL is ready, installing Atlas CLI...'
if ! command -v atlas >/dev/null 2>&1; then
@ -53,7 +53,7 @@ services:
else
echo 'Atlas CLI already installed'
fi
if [ -f '/opencoze_latest_schema.hcl' ]; then
echo 'Running Atlas migrations...'
ATLAS_URL="mysql://$${MYSQL_USER}:$${MYSQL_PASSWORD}@localhost:3306/$${MYSQL_DATABASE}"
@ -274,7 +274,7 @@ services:
# Download plugin package locally
echo 'Copying smartcn plugin...';
cp /opt/bitnami/elasticsearch/analysis-smartcn.zip /tmp/analysis-smartcn.zip
cp /opt/bitnami/elasticsearch/analysis-smartcn.zip /tmp/analysis-smartcn.zip
elasticsearch-plugin install file:///tmp/analysis-smartcn.zip
if [[ "$$?" != "0" ]]; then

View File

@ -252,6 +252,7 @@ services:
OB_DATAFILE_SIZE: 1G
OB_SYS_PASSWORD: ${OCEANBASE_PASSWORD:-coze123}
OB_TENANT_PASSWORD: ${OCEANBASE_PASSWORD:-coze123}
OB_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-cozeAi}
ports:
- '2881:2881'
volumes:

View File

@ -345,6 +345,7 @@ services:
OB_DATAFILE_SIZE: 1G
OB_SYS_PASSWORD: ${OCEANBASE_PASSWORD:-coze123}
OB_TENANT_PASSWORD: ${OCEANBASE_PASSWORD:-coze123}
OB_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-cozeAi}
profiles: ['middleware']
env_file: *env_file
ports:

View File

@ -19,7 +19,7 @@ services:
- ./data/mysql:/var/lib/mysql
- ./volumes/mysql/schema.sql:/docker-entrypoint-initdb.d/init.sql
- ./atlas/opencoze_latest_schema.hcl:/opencoze_latest_schema.hcl:ro
entrypoint:
entrypoint:
- bash
- -c
- |
@ -42,7 +42,7 @@ services:
sleep 2
fi
done
echo 'MySQL is ready, installing Atlas CLI...'
if ! command -v atlas >/dev/null 2>&1; then
@ -52,7 +52,7 @@ services:
else
echo 'Atlas CLI already installed'
fi
if [ -f '/opencoze_latest_schema.hcl' ]; then
echo 'Running Atlas migrations...'
ATLAS_URL="mysql://$${MYSQL_USER}:$${MYSQL_PASSWORD}@localhost:3306/$${MYSQL_DATABASE}"
@ -161,7 +161,7 @@ services:
# Download plugin package locally
echo 'Copying smartcn plugin...';
cp /opt/bitnami/elasticsearch/analysis-smartcn.zip /tmp/analysis-smartcn.zip
cp /opt/bitnami/elasticsearch/analysis-smartcn.zip /tmp/analysis-smartcn.zip
elasticsearch-plugin install file:///tmp/analysis-smartcn.zip
if [[ "$$?" != "0" ]]; then

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Some files were not shown because too many files have changed in this diff Show More