feat: Support for Chat Flow & Agent Support for binding a single chat flow (#765)
Co-authored-by: Yu Yang <72337138+tomasyu985@users.noreply.github.com> Co-authored-by: zengxiaohui <csu.zengxiaohui@gmail.com> Co-authored-by: lijunwen.gigoo <lijunwen.gigoo@bytedance.com> Co-authored-by: lvxinyu.1117 <lvxinyu.1117@bytedance.com> Co-authored-by: liuyunchao.0510 <liuyunchao.0510@bytedance.com> Co-authored-by: haozhenfei <37089575+haozhenfei@users.noreply.github.com> Co-authored-by: July <jiangxujin@bytedance.com> Co-authored-by: tecvan-fe <fanwenjie.fe@bytedance.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/hertz-contrib/sse"
|
||||
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
"github.com/cloudwego/hertz/pkg/protocol/consts"
|
||||
|
||||
"github.com/coze-dev/coze-studio/backend/api/model/conversation/run"
|
||||
|
||||
@@ -123,3 +124,23 @@ func checkParamsV3(_ context.Context, ar *run.ChatV3Request) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelChatApi .
|
||||
// @router /v3/chat/cancel [POST]
|
||||
func CancelChatApi(ctx context.Context, c *app.RequestContext) {
|
||||
var err error
|
||||
var req run.CancelChatApiRequest
|
||||
err = c.BindAndValidate(&req)
|
||||
if err != nil {
|
||||
invalidParamRequestResponse(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := conversation.ConversationOpenAPISVC.CancelRun(ctx, &req)
|
||||
if err != nil {
|
||||
invalidParamRequestResponse(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
openapiauthApp "github.com/coze-dev/coze-studio/backend/application/openauth"
|
||||
"github.com/coze-dev/coze-studio/backend/application/plugin"
|
||||
"github.com/coze-dev/coze-studio/backend/application/singleagent"
|
||||
"github.com/coze-dev/coze-studio/backend/application/upload"
|
||||
@@ -73,7 +74,7 @@ func UploadFileOpen(ctx context.Context, c *app.RequestContext) {
|
||||
var req bot_open_api.UploadFileOpenRequest
|
||||
err = c.BindAndValidate(&req)
|
||||
if err != nil {
|
||||
c.String(consts.StatusBadRequest, err.Error())
|
||||
invalidParamRequestResponse(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -83,6 +84,7 @@ func UploadFileOpen(ctx context.Context, c *app.RequestContext) {
|
||||
internalServerErrorResponse(ctx, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -93,7 +95,7 @@ func GetBotOnlineInfo(ctx context.Context, c *app.RequestContext) {
|
||||
var req bot_open_api.GetBotOnlineInfoReq
|
||||
err = c.BindAndValidate(&req)
|
||||
if err != nil {
|
||||
c.String(consts.StatusBadRequest, err.Error())
|
||||
invalidParamRequestResponse(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -105,3 +107,43 @@ func GetBotOnlineInfo(ctx context.Context, c *app.RequestContext) {
|
||||
}
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
|
||||
// ImpersonateCozeUser .
|
||||
// @router /api/permission_api/coze_web_app/impersonate_coze_user [POST]
|
||||
func ImpersonateCozeUser(ctx context.Context, c *app.RequestContext) {
|
||||
var err error
|
||||
var req bot_open_api.ImpersonateCozeUserRequest
|
||||
err = c.BindAndValidate(&req)
|
||||
if err != nil {
|
||||
invalidParamRequestResponse(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := openapiauthApp.OpenAuthApplication.ImpersonateCozeUserAccessToken(ctx, &req)
|
||||
|
||||
if err != nil {
|
||||
internalServerErrorResponse(ctx, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
|
||||
// OpenGetBotInfo .
|
||||
// @router /v1/bots/:bot_id [GET]
|
||||
func OpenGetBotInfo(ctx context.Context, c *app.RequestContext) {
|
||||
var err error
|
||||
var req bot_open_api.OpenGetBotInfoRequest
|
||||
err = c.BindAndValidate(&req)
|
||||
if err != nil {
|
||||
invalidParamRequestResponse(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := singleagent.SingleAgentSVC.OpenGetBotInfo(ctx, &req)
|
||||
if err != nil {
|
||||
internalServerErrorResponse(ctx, c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -170,3 +170,42 @@ func ListConversationsApi(ctx context.Context, c *app.RequestContext) {
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
|
||||
// UpdateConversationApi .
|
||||
// @router /v1/conversations/:conversation_id [PUT]
|
||||
func UpdateConversationApi(ctx context.Context, c *app.RequestContext) {
|
||||
var err error
|
||||
var req conversation.UpdateConversationApiRequest
|
||||
err = c.BindAndValidate(&req)
|
||||
if err != nil {
|
||||
invalidParamRequestResponse(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := application.ConversationSVC.UpdateConversation(ctx, &req)
|
||||
if err != nil {
|
||||
internalServerErrorResponse(ctx, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
|
||||
// DeleteConversationApi .
|
||||
// @router /v1/conversations/:conversation_id [DELETE]
|
||||
func DeleteConversationApi(ctx context.Context, c *app.RequestContext) {
|
||||
var err error
|
||||
var req conversation.DeleteConversationApiRequest
|
||||
err = c.BindAndValidate(&req)
|
||||
if err != nil {
|
||||
invalidParamRequestResponse(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp, err := application.ConversationSVC.DeleteConversation(ctx, &req)
|
||||
if err != nil {
|
||||
internalServerErrorResponse(ctx, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -404,3 +404,23 @@ func DraftProjectCopy(ctx context.Context, c *app.RequestContext) {
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
|
||||
// GetOnlineAppData .
|
||||
// @router /v1/apps/:app_id [GET]
|
||||
func GetOnlineAppData(ctx context.Context, c *app.RequestContext) {
|
||||
var err error
|
||||
var req project.GetOnlineAppDataRequest
|
||||
err = c.BindAndValidate(&req)
|
||||
if err != nil {
|
||||
c.String(consts.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := appApplication.APPApplicationSVC.GetOnlineAppData(ctx, &req)
|
||||
if err != nil {
|
||||
internalServerErrorResponse(ctx, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ func GetApiMessageList(ctx context.Context, c *app.RequestContext) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := application.OpenapiMessageApplicationService.GetApiMessageList(ctx, &req)
|
||||
resp, err := application.OpenapiMessageSVC.GetApiMessageList(ctx, &req)
|
||||
if err != nil {
|
||||
internalServerErrorResponse(ctx, c, err)
|
||||
return
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
* 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 hertz generator.
|
||||
|
||||
package coze
|
||||
@@ -21,7 +37,7 @@ func CommonUpload(ctx context.Context, c *app.RequestContext) {
|
||||
var req upload.CommonUploadRequest
|
||||
err = c.BindAndValidate(&req)
|
||||
if err != nil {
|
||||
c.String(consts.StatusBadRequest, err.Error())
|
||||
invalidParamRequestResponse(c, err.Error())
|
||||
return
|
||||
}
|
||||
fullUrl := string(c.Request.URI().FullURI())
|
||||
@@ -41,7 +57,7 @@ func ApplyUploadAction(ctx context.Context, c *app.RequestContext) {
|
||||
var req upload.ApplyUploadActionRequest
|
||||
err = c.BindAndValidate(&req)
|
||||
if err != nil {
|
||||
c.String(consts.StatusBadRequest, err.Error())
|
||||
invalidParamRequestResponse(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp := new(upload.ApplyUploadActionResponse)
|
||||
|
||||
@@ -23,6 +23,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
@@ -555,7 +557,11 @@ func CreateProjectConversationDef(ctx context.Context, c *app.RequestContext) {
|
||||
return
|
||||
}
|
||||
|
||||
resp := new(workflow.CreateProjectConversationDefResponse)
|
||||
resp, err := appworkflow.SVC.CreateApplicationConversationDef(ctx, &req)
|
||||
if err != nil {
|
||||
internalServerErrorResponse(ctx, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
@@ -570,8 +576,11 @@ func UpdateProjectConversationDef(ctx context.Context, c *app.RequestContext) {
|
||||
invalidParamRequestResponse(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp := new(workflow.UpdateProjectConversationDefResponse)
|
||||
resp, err := appworkflow.SVC.UpdateApplicationConversationDef(ctx, &req)
|
||||
if err != nil {
|
||||
internalServerErrorResponse(ctx, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
@@ -587,7 +596,11 @@ func DeleteProjectConversationDef(ctx context.Context, c *app.RequestContext) {
|
||||
return
|
||||
}
|
||||
|
||||
resp := new(workflow.DeleteProjectConversationDefResponse)
|
||||
resp, err := appworkflow.SVC.DeleteApplicationConversationDef(ctx, &req)
|
||||
if err != nil {
|
||||
internalServerErrorResponse(ctx, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
@@ -603,7 +616,11 @@ func ListProjectConversationDef(ctx context.Context, c *app.RequestContext) {
|
||||
return
|
||||
}
|
||||
|
||||
resp := new(workflow.ListProjectConversationResponse)
|
||||
resp, err := appworkflow.SVC.ListApplicationConversationDef(ctx, &req)
|
||||
if err != nil {
|
||||
internalServerErrorResponse(ctx, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
@@ -723,7 +740,11 @@ func GetChatFlowRole(ctx context.Context, c *app.RequestContext) {
|
||||
return
|
||||
}
|
||||
|
||||
resp := new(workflow.GetChatFlowRoleResponse)
|
||||
resp, err := appworkflow.SVC.GetChatFlowRole(ctx, &req)
|
||||
if err != nil {
|
||||
internalServerErrorResponse(ctx, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
@@ -738,8 +759,11 @@ func CreateChatFlowRole(ctx context.Context, c *app.RequestContext) {
|
||||
invalidParamRequestResponse(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp := new(workflow.CreateChatFlowRoleResponse)
|
||||
resp, err := appworkflow.SVC.CreateChatFlowRole(ctx, &req)
|
||||
if err != nil {
|
||||
internalServerErrorResponse(ctx, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
@@ -755,7 +779,11 @@ func DeleteChatFlowRole(ctx context.Context, c *app.RequestContext) {
|
||||
return
|
||||
}
|
||||
|
||||
resp := new(workflow.DeleteChatFlowRoleResponse)
|
||||
resp, err := appworkflow.SVC.DeleteChatFlowRole(ctx, &req)
|
||||
if err != nil {
|
||||
internalServerErrorResponse(ctx, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
@@ -1061,6 +1089,11 @@ func OpenAPIGetWorkflowRunHistory(ctx context.Context, c *app.RequestContext) {
|
||||
// @router /v1/workflows/chat [POST]
|
||||
func OpenAPIChatFlowRun(ctx context.Context, c *app.RequestContext) {
|
||||
var err error
|
||||
if err = preprocessWorkflowRequestBody(ctx, c); err != nil {
|
||||
invalidParamRequestResponse(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var req workflow.ChatFlowRunRequest
|
||||
err = c.BindAndValidate(&req)
|
||||
if err != nil {
|
||||
@@ -1068,27 +1101,107 @@ func OpenAPIChatFlowRun(ctx context.Context, c *app.RequestContext) {
|
||||
return
|
||||
}
|
||||
|
||||
resp := new(workflow.ChatFlowRunResponse)
|
||||
w := sse.NewWriter(c)
|
||||
c.SetContentType("text/event-stream; charset=utf-8")
|
||||
c.Response.Header.Set("Cache-Control", "no-cache")
|
||||
c.Response.Header.Set("Connection", "keep-alive")
|
||||
c.Response.Header.Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
sr, err := appworkflow.SVC.OpenAPIChatFlowRun(ctx, &req)
|
||||
if err != nil {
|
||||
internalServerErrorResponse(ctx, c, err)
|
||||
return
|
||||
}
|
||||
sendChatFlowStreamRunSSE(ctx, w, sr)
|
||||
|
||||
}
|
||||
|
||||
func sendChatFlowStreamRunSSE(ctx context.Context, w *sse.Writer, sr *schema.StreamReader[[]*workflow.ChatFlowRunResponse]) {
|
||||
defer func() {
|
||||
_ = w.Close()
|
||||
sr.Close()
|
||||
}()
|
||||
seq := int64(1)
|
||||
for {
|
||||
respList, err := sr.Recv()
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
// finish
|
||||
break
|
||||
}
|
||||
|
||||
event := &sse.Event{
|
||||
Type: "error",
|
||||
Data: []byte(err.Error()),
|
||||
}
|
||||
|
||||
if err = w.Write(event); err != nil {
|
||||
logs.CtxErrorf(ctx, "publish stream event failed, err:%v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, resp := range respList {
|
||||
event := &sse.Event{
|
||||
ID: strconv.FormatInt(seq, 10),
|
||||
Type: resp.Event,
|
||||
Data: []byte(resp.Data),
|
||||
}
|
||||
|
||||
if err = w.Write(event); err != nil {
|
||||
logs.CtxErrorf(ctx, "publish stream event failed, err:%v", err)
|
||||
return
|
||||
}
|
||||
seq++
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAPIGetWorkflowInfo .
|
||||
// @router /v1/workflows/:workflow_id [GET]
|
||||
func OpenAPIGetWorkflowInfo(ctx context.Context, c *app.RequestContext) {
|
||||
var err error
|
||||
|
||||
if err = processOpenAPIGetWorkflowInfoRequest(ctx, c); err != nil {
|
||||
invalidParamRequestResponse(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var req workflow.OpenAPIGetWorkflowInfoRequest
|
||||
|
||||
err = c.BindAndValidate(&req)
|
||||
if err != nil {
|
||||
invalidParamRequestResponse(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp := new(workflow.OpenAPIGetWorkflowInfoResponse)
|
||||
resp, err := appworkflow.SVC.OpenAPIGetWorkflowInfo(ctx, &req)
|
||||
if err != nil {
|
||||
internalServerErrorResponse(ctx, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
|
||||
func processOpenAPIGetWorkflowInfoRequest(_ context.Context, c *app.RequestContext) error {
|
||||
queryString := c.Request.QueryString()
|
||||
|
||||
values, err := url.ParseQuery(string(queryString))
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse query parameter failed, err:%v", err)
|
||||
}
|
||||
isDebug := values.Get("is_debug")
|
||||
if len(isDebug) == 0 {
|
||||
values.Set("is_debug", "false")
|
||||
}
|
||||
c.Request.SetQueryString(values.Encode())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetHistorySchema .
|
||||
// @router /api/workflow_api/history_schema [POST]
|
||||
func GetHistorySchema(ctx context.Context, c *app.RequestContext) {
|
||||
@@ -1128,3 +1241,22 @@ func GetExampleWorkFlowList(ctx context.Context, c *app.RequestContext) {
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
|
||||
// OpenAPICreateConversation .
|
||||
// @router /v1/workflow/conversation/create [POST]
|
||||
func OpenAPICreateConversation(ctx context.Context, c *app.RequestContext) {
|
||||
var err error
|
||||
var req workflow.CreateConversationRequest
|
||||
err = c.BindAndValidate(&req)
|
||||
if err != nil {
|
||||
c.String(consts.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
resp, err := appworkflow.SVC.OpenAPICreateConversation(ctx, &req)
|
||||
if err != nil {
|
||||
internalServerErrorResponse(ctx, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(consts.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
@@ -42,6 +43,7 @@ import (
|
||||
"github.com/cloudwego/hertz/pkg/common/ut"
|
||||
"github.com/cloudwego/hertz/pkg/protocol"
|
||||
"github.com/cloudwego/hertz/pkg/protocol/sse"
|
||||
"github.com/coze-dev/coze-studio/backend/domain/workflow/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
@@ -250,7 +252,10 @@ func newWfTestRunner(t *testing.T) *wfTestRunner {
|
||||
|
||||
mockTos := storageMock.NewMockStorage(ctrl)
|
||||
mockTos.EXPECT().GetObjectUrl(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).AnyTimes()
|
||||
workflowRepo := service.NewWorkflowRepository(mockIDGen, db, redisClient, mockTos, cpStore, utChatModel, nil)
|
||||
|
||||
workflowRepo, _ := service.NewWorkflowRepository(mockIDGen, db, redisClient, mockTos, cpStore, utChatModel, &config.WorkflowConfig{
|
||||
NodeOfCodeConfig: &config.NodeOfCodeConfig{},
|
||||
})
|
||||
mockey.Mock(appworkflow.GetWorkflowDomainSVC).Return(service.NewWorkflowService(workflowRepo)).Build()
|
||||
mockey.Mock(workflow2.GetRepository).Return(workflowRepo).Build()
|
||||
publishPatcher := mockey.Mock(appworkflow.PublishWorkflowResource).Return(nil).Build()
|
||||
@@ -4100,13 +4105,13 @@ func TestCopyWorkflowAppToLibrary(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
validateSubWorkflowIDs(subworkflowCanvas.Nodes)
|
||||
case entity.NodeTypeLLM:
|
||||
if node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.WorkflowFCParam != nil {
|
||||
if node.Data.Inputs.LLM != nil && node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.WorkflowFCParam != nil {
|
||||
for _, w := range node.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList {
|
||||
assert.True(t, copiedIDMap[w.WorkflowID])
|
||||
}
|
||||
}
|
||||
|
||||
if node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.PluginFCParam != nil {
|
||||
if node.Data.Inputs.LLM != nil && node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.PluginFCParam != nil {
|
||||
for _, p := range node.Data.Inputs.FCParam.PluginFCParam.PluginList {
|
||||
if p.PluginVersion == "0" {
|
||||
assert.Equal(t, "100100", p.PluginID)
|
||||
@@ -4114,7 +4119,7 @@ func TestCopyWorkflowAppToLibrary(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.KnowledgeFCParam != nil {
|
||||
if node.Data.Inputs.LLM != nil && node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.KnowledgeFCParam != nil {
|
||||
for _, k := range node.Data.Inputs.FCParam.KnowledgeFCParam.KnowledgeList {
|
||||
assert.Equal(t, "100100", k.ID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user