From a244b8b459edbe396309a4d15bfbdbe472e8ddb7 Mon Sep 17 00:00:00 2001 From: 0xff-dev Date: Tue, 2 Apr 2024 16:31:25 +0800 Subject: [PATCH] refactor: save app icon to oss --- api/base/v1alpha1/application_types.go | 2 +- apiserver/pkg/application/application.go | 81 +++++++++++++++---- apiserver/pkg/chat/chat_server.go | 7 +- apiserver/pkg/common/common.go | 15 ++++ apiserver/pkg/gpt/gpt.go | 9 ++- ...cadia.kubeagi.k8s.com.cn_applications.yaml | 8 +- ...cadia.kubeagi.k8s.com.cn_applications.yaml | 8 +- pkg/config/config.go | 13 +++ pkg/utils/image.go | 13 +++ 9 files changed, 123 insertions(+), 33 deletions(-) create mode 100644 pkg/utils/image.go diff --git a/api/base/v1alpha1/application_types.go b/api/base/v1alpha1/application_types.go index 1f7670db9..247cf07f0 100644 --- a/api/base/v1alpha1/application_types.go +++ b/api/base/v1alpha1/application_types.go @@ -32,7 +32,7 @@ type NodeConfig struct { type ApplicationSpec struct { CommonSpec `json:",inline"` // Icon base64 image icon - Icon string `json:"icon,omitempty"` + // Icon string `json:"icon,omitempty"` // IsPublic Set whether the current application provides services to the public IsPublic bool `json:"isPublic,omitempty"` // IsRecommended Set whether the current application is recognized as recommended to users diff --git a/apiserver/pkg/application/application.go b/apiserver/pkg/application/application.go index 3d50ce2cb..cd06ec0cd 100644 --- a/apiserver/pkg/application/application.go +++ b/apiserver/pkg/application/application.go @@ -17,12 +17,14 @@ limitations under the License. package application import ( + "bytes" "context" "errors" "fmt" "reflect" "strings" + "github.com/minio/minio-go/v7" "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -40,6 +42,9 @@ import ( "github.com/kubeagi/arcadia/apiserver/graph/generated" "github.com/kubeagi/arcadia/apiserver/pkg/common" "github.com/kubeagi/arcadia/apiserver/pkg/utils" + "github.com/kubeagi/arcadia/pkg/config" + "github.com/kubeagi/arcadia/pkg/datasource" + pkgutils "github.com/kubeagi/arcadia/pkg/utils" ) func addCategory(app *v1alpha1.Application, category []*string) *v1alpha1.Application { @@ -68,7 +73,7 @@ func addDefaultValue(gApp *generated.Application, app *v1alpha1.Application) { gApp.ConversionWindowSize = pointer.Int(5) } -func cr2app(prompt *apiprompt.Prompt, chainConfig *apichain.CommonChainConfig, retriever *apiretriever.CommonRetrieverConfig, app *v1alpha1.Application, agent *apiagent.Agent, doc *apidocumentloader.DocumentLoader, enableRerank, enableMultiQuery *bool, rerankModel *string) (*generated.Application, error) { +func cr2app(ctx context.Context, c client.Client, prompt *apiprompt.Prompt, chainConfig *apichain.CommonChainConfig, retriever *apiretriever.CommonRetrieverConfig, app *v1alpha1.Application, agent *apiagent.Agent, doc *apidocumentloader.DocumentLoader, enableRerank, enableMultiQuery *bool, rerankModel *string) (*generated.Application, error) { if app == nil { return nil, errors.New("no app found") } @@ -76,6 +81,7 @@ func cr2app(prompt *apiprompt.Prompt, chainConfig *apichain.CommonChainConfig, r UpdateTimestamp := &condition.LastTransitionTime.Time status := common.GetObjStatus(app) + icon, _ := common.AppIconLink(ctx, app, c) gApp := &generated.Application{ Metadata: &generated.ApplicationMetadata{ Name: app.Name, @@ -85,7 +91,7 @@ func cr2app(prompt *apiprompt.Prompt, chainConfig *apichain.CommonChainConfig, r Annotations: utils.MapStr2Any(app.Annotations), DisplayName: pointer.String(app.Spec.DisplayName), Description: pointer.String(app.Spec.Description), - Icon: pointer.String(app.Spec.Icon), + Icon: &icon, Creator: pointer.String(app.Spec.Creator), CreationTimestamp: &app.CreationTimestamp.Time, UpdateTimestamp: UpdateTimestamp, @@ -147,19 +153,27 @@ func cr2app(prompt *apiprompt.Prompt, chainConfig *apichain.CommonChainConfig, r return gApp, nil } -func app2metadataConverter(objApp client.Object) (generated.PageNode, error) { - app, ok := objApp.(*v1alpha1.Application) - if !ok { - return nil, errors.New("can't convert client.Object to Application") +func appConverterHelper(ctx context.Context, c client.Client) common.ResourceConverter { + return func(objApp client.Object) (generated.PageNode, error) { + app, ok := objApp.(*v1alpha1.Application) + if !ok { + return nil, errors.New("can't convert client.Object to Application") + } + return app2metadata(ctx, c, app) } - return app2metadata(app) } -func app2metadata(app *v1alpha1.Application) (*generated.ApplicationMetadata, error) { +/* +func app2metadataConverter(ctx context.Context, c client.Client, objApp client.Object) (generated.PageNode, error) { + +} +*/ +func app2metadata(ctx context.Context, c client.Client, app *v1alpha1.Application) (*generated.ApplicationMetadata, error) { condition := app.Status.GetCondition(v1alpha1.TypeReady) UpdateTimestamp := &condition.LastTransitionTime.Time status := common.GetObjStatus(app) + icon, _ := common.AppIconLink(ctx, app, c) return &generated.ApplicationMetadata{ Name: app.Name, Namespace: app.Namespace, @@ -171,7 +185,7 @@ func app2metadata(app *v1alpha1.Application) (*generated.ApplicationMetadata, er Description: pointer.String(app.Spec.Description), CreationTimestamp: &app.CreationTimestamp.Time, UpdateTimestamp: UpdateTimestamp, - Icon: pointer.String(app.Spec.Icon), + Icon: &icon, IsPublic: pointer.Bool(app.Spec.IsPublic), IsRecommended: pointer.Bool(app.Spec.IsRecommended), Status: pointer.String(status), @@ -192,7 +206,6 @@ func CreateApplication(ctx context.Context, c client.Client, input generated.Cre DisplayName: input.DisplayName, Description: pointer.StringPtrDerefOr(input.Description, ""), }, - Icon: input.Icon, IsPublic: pointer.BoolDeref(input.IsPublic, false), IsRecommended: pointer.BoolDeref(input.IsRecommended, false), Prologue: "", @@ -202,11 +215,16 @@ func CreateApplication(ctx context.Context, c client.Client, input generated.Cre if len(input.Category) > 0 { app = addCategory(app, input.Category) } + + _, err := UploadIcon(ctx, c, input.Icon, input.Name, input.Namespace) + if err != nil { + return nil, err + } common.SetCreator(ctx, &app.Spec.CommonSpec) if err := c.Create(ctx, app); err != nil { return nil, err } - return app2metadata(app) + return app2metadata(ctx, c, app) } func UpdateApplication(ctx context.Context, c client.Client, input generated.UpdateApplicationMetadataInput) (*generated.ApplicationMetadata, error) { @@ -226,15 +244,19 @@ func UpdateApplication(ctx context.Context, c client.Client, input generated.Upd } app.Spec.DisplayName = input.DisplayName app.Spec.Description = pointer.StringDeref(input.Description, app.Spec.Description) - app.Spec.Icon = input.Icon + // app.Spec.Icon = input.Icon app.Spec.IsPublic = pointer.BoolDeref(input.IsPublic, app.Spec.IsPublic) app.Spec.IsRecommended = pointer.BoolDeref(input.IsRecommended, app.Spec.IsRecommended) + _, err := UploadIcon(ctx, c, input.Icon, input.Name, input.Namespace) + if err != nil { + return nil, err + } if !reflect.DeepEqual(app, oldApp) { if err := c.Update(ctx, app); err != nil { return nil, err } } - return app2metadata(app) + return app2metadata(ctx, c, app) } func DeleteApplication(ctx context.Context, c client.Client, input generated.DeleteCommonInput) (*string, error) { @@ -386,7 +408,7 @@ func GetApplication(ctx context.Context, c client.Client, name, namespace string return nil, err } - return cr2app(prompt, chainConfig, retriever, app, agent, doc, pointer.Bool(enableRerankRetriever), pointer.Bool(enableMultiQueryRetriever), pointer.String(rerankModel)) + return cr2app(ctx, c, prompt, chainConfig, retriever, app, agent, doc, pointer.Bool(enableRerankRetriever), pointer.Bool(enableMultiQueryRetriever), pointer.String(rerankModel)) } func ListApplicationMeatadatas(ctx context.Context, c client.Client, input generated.ListCommonInput) (*generated.PaginatedResult, error) { @@ -409,7 +431,7 @@ func ListApplicationMeatadatas(ctx context.Context, c client.Client, input gener for i := range res.Items { items[i] = &res.Items[i] } - return common.ListReources(items, page, pageSize, app2metadataConverter, filter...) + return common.ListReources(items, page, pageSize, appConverterHelper(ctx, c), filter...) } func UpdateApplicationConfig(ctx context.Context, c client.Client, input generated.UpdateApplicationConfigInput) (*generated.Application, error) { @@ -743,7 +765,7 @@ func UpdateApplicationConfig(ctx context.Context, c client.Client, input generat } } - return cr2app(prompt, chainConfig, retriever, app, agent, documentLoader, pointer.Bool(hasRerankRetriever), pointer.Bool(hasMultiQueryRetriever), pointer.String(rerankModel)) + return cr2app(ctx, c, prompt, chainConfig, retriever, app, agent, documentLoader, pointer.Bool(hasRerankRetriever), pointer.Bool(hasMultiQueryRetriever), pointer.String(rerankModel)) } func mutateApp(app *v1alpha1.Application, input generated.UpdateApplicationConfigInput, hasMultiQueryRetriever, hasRerankRetriever bool) error { @@ -963,3 +985,30 @@ func redefineNodes(knowledgebase *string, namespace string, name string, llmName }) return nodes } + +func UploadIcon(ctx context.Context, client client.Client, icon, appName, namespace string) (string, error) { + if strings.HasPrefix(icon, "data:image") { + imgBytes, err := pkgutils.ParseBase64ImageBytes(icon) + if err != nil { + return "", err + } + + system, err := config.GetSystemDatasource(ctx, client) + if err != nil { + return "", err + } + + endpoint := system.Spec.Endpoint.DeepCopy() + if endpoint != nil && endpoint.AuthSecret != nil { + endpoint.AuthSecret.WithNameSpace(namespace) + } + ds, err := datasource.NewOSS(ctx, client, endpoint) + if err != nil { + return "", err + } + iconName := fmt.Sprintf("application/%s/icon/%s", appName, appName) + _, err = ds.Client.PutObject(ctx, namespace, iconName, bytes.NewReader(imgBytes), int64(len(imgBytes)), minio.PutObjectOptions{}) + return icon, err + } + return icon, nil +} diff --git a/apiserver/pkg/chat/chat_server.go b/apiserver/pkg/chat/chat_server.go index 4fec672e6..1600eae36 100644 --- a/apiserver/pkg/chat/chat_server.go +++ b/apiserver/pkg/chat/chat_server.go @@ -40,6 +40,7 @@ import ( "github.com/kubeagi/arcadia/apiserver/pkg/auth" "github.com/kubeagi/arcadia/apiserver/pkg/chat/storage" "github.com/kubeagi/arcadia/apiserver/pkg/client" + "github.com/kubeagi/arcadia/apiserver/pkg/common" "github.com/kubeagi/arcadia/pkg/appruntime" "github.com/kubeagi/arcadia/pkg/appruntime/base" appruntimechain "github.com/kubeagi/arcadia/pkg/appruntime/chain" @@ -430,14 +431,16 @@ func (cs *ChatServer) FillAppIconToConversations(ctx context.Context, conversati if !ok { return nil } - err := cs.cli.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, app) + app.Name = name + app.Namespace = ns + link, err := common.AppIconLink(ctx, app, cs.cli) if err != nil { // FIXME: Currently, there is a request for an application that cannot be found in a conversation in the database, // causing other conversations to be unable to add icons, so an error is encountered here and no error is returned. klog.Errorf("failed to get application %s in namespace %s, error %s", name, ns, err) return nil } - result[index] = app.Spec.Icon + result[index] = link return nil }) } diff --git a/apiserver/pkg/common/common.go b/apiserver/pkg/common/common.go index 06405f3d2..fd0295004 100644 --- a/apiserver/pkg/common/common.go +++ b/apiserver/pkg/common/common.go @@ -20,7 +20,9 @@ import ( "context" "errors" "fmt" + "net/url" "strings" + "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" @@ -306,3 +308,16 @@ func NewListOptions(input generated.ListCommonInput) ([]client.ListOption, error } return opts, nil } + +func AppIconLink(ctx context.Context, app *v1alpha1.Application, client client.Client) (string, error) { + ds, err := SystemDatasourceOSS(ctx, client) + if err != nil { + return "", err + } + name := fmt.Sprintf("application/%s/icon/%s", app.Name, app.Name) + u, err := ds.Client.PresignedGetObject(ctx, app.Namespace, name, 24*time.Hour, url.Values{}) + if err != nil { + return "", err + } + return u.String(), nil +} diff --git a/apiserver/pkg/gpt/gpt.go b/apiserver/pkg/gpt/gpt.go index a6e3963db..321550f10 100644 --- a/apiserver/pkg/gpt/gpt.go +++ b/apiserver/pkg/gpt/gpt.go @@ -41,11 +41,12 @@ var ( chatStorage storage.Storage ) -func app2gpt(app *v1alpha1.Application, c client.Client) (*generated.Gpt, error) { +func app2gpt(ctx context.Context, app *v1alpha1.Application, c client.Client) (*generated.Gpt, error) { if app == nil { return nil, errors.New("no app found") } + icon, _ := common.AppIconLink(ctx, app, c) gpt := &generated.Gpt{ Name: pointer.String(strings.Join([]string{app.Namespace, app.Name}, "/")), DisplayName: pointer.String(app.Spec.DisplayName), @@ -53,7 +54,7 @@ func app2gpt(app *v1alpha1.Application, c client.Client) (*generated.Gpt, error) Hot: pointer.Int64(getHot(app, c)), Creator: pointer.String(app.Spec.Creator), Category: common.GetAppCategory(app), - Icon: pointer.String(app.Spec.Icon), + Icon: &icon, Prologue: pointer.String(app.Spec.Prologue), ShowRespInfo: pointer.Bool(app.Spec.ShowRespInfo), ShowRetrievalInfo: pointer.Bool(app.Spec.ShowRetrievalInfo), @@ -144,7 +145,7 @@ func GetGPT(ctx context.Context, c client.Client, name string) (*generated.Gpt, return nil, fmt.Errorf("not a valid app or the app is not public") } - return app2gpt(app, c) + return app2gpt(ctx, app, c) } // ListGPT list all gpt @@ -179,7 +180,7 @@ func ListGPT(ctx context.Context, c client.Client, input generated.ListGPTInput) if !ok { return nil, errors.New("can't convert obj to Application") } - return app2gpt(app, c) + return app2gpt(ctx, app, c) }, filter...) } diff --git a/config/crd/bases/arcadia.kubeagi.k8s.com.cn_applications.yaml b/config/crd/bases/arcadia.kubeagi.k8s.com.cn_applications.yaml index 4a51a9182..c84553a02 100644 --- a/config/crd/bases/arcadia.kubeagi.k8s.com.cn_applications.yaml +++ b/config/crd/bases/arcadia.kubeagi.k8s.com.cn_applications.yaml @@ -61,12 +61,10 @@ spec: enableUploadFile: default: true type: boolean - icon: - description: Icon base64 image icon - type: string isPublic: - description: IsPublic Set whether the current application provides - services to the public + description: Icon base64 image icon Icon string `json:"icon,omitempty"` + IsPublic Set whether the current application provides services to + the public type: boolean isRecommended: description: IsRecommended Set whether the current application is diff --git a/deploy/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_applications.yaml b/deploy/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_applications.yaml index 4a51a9182..c84553a02 100644 --- a/deploy/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_applications.yaml +++ b/deploy/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_applications.yaml @@ -61,12 +61,10 @@ spec: enableUploadFile: default: true type: boolean - icon: - description: Icon base64 image icon - type: string isPublic: - description: IsPublic Set whether the current application provides - services to the public + description: Icon base64 image icon Icon string `json:"icon,omitempty"` + IsPublic Set whether the current application provides services to + the public type: boolean isRecommended: description: IsRecommended Set whether the current application is diff --git a/pkg/config/config.go b/pkg/config/config.go index 0b16982da..b82ea9d3b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" arcadiav1alpha1 "github.com/kubeagi/arcadia/api/base/v1alpha1" + "github.com/kubeagi/arcadia/pkg/datasource" "github.com/kubeagi/arcadia/pkg/utils" ) @@ -162,3 +163,15 @@ func GetDefaultRerankModel(ctx context.Context, c client.Client) (*arcadiav1alph } return config.Rerank, nil } + +func GetSystemDatasourceOSS(ctx context.Context, mgrClient client.Client) (*datasource.OSS, error) { + systemDatasource, err := GetSystemDatasource(ctx, mgrClient) + if err != nil { + return nil, err + } + endpoint := systemDatasource.Spec.Endpoint.DeepCopy() + if endpoint.AuthSecret != nil && endpoint.AuthSecret.Namespace == nil { + endpoint.AuthSecret.WithNameSpace(systemDatasource.Namespace) + } + return datasource.NewOSS(ctx, mgrClient, endpoint) +} diff --git a/pkg/utils/image.go b/pkg/utils/image.go new file mode 100644 index 000000000..834248d85 --- /dev/null +++ b/pkg/utils/image.go @@ -0,0 +1,13 @@ +package utils + +import ( + "encoding/base64" +) + +func ParseBase64ImageBytes(img string) ([]byte, error) { + i := 0 + for ; i < len(img) && img[i] != ','; i++ { + } + cut := img[i+1:] + return base64.StdEncoding.DecodeString(cut) +}