diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..88137b31 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,98 @@ +variables: + PATH: /opt/go-1.21/go/bin/:/opt/node/node/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin + GOROOT: /opt/go-1.21/go + GOPROXY: https://goproxy.cn + VERSION: $CI_COMMIT_SHORT_SHA + APP: apipark + APP_PRE: ${APP}_${VERSION} + BUILD_DIR: ${APP}-build + DEPLOY_DESC: "DEV 环境" + VIEW_ADDR: http://172.18.166.219:8288 + SAVE_DIR: /opt/${APP} + NODE_OPTIONS: --max_old_space_size=8192 + +stages: + - notice + - prefix + - build + - deploy + - webhook + +feishu-informer: # 飞书回调 + stage: notice + variables: + DIFF_URL: "$CI_MERGE_REQUEST_PROJECT_URL/-/merge_requests/$CI_MERGE_REQUEST_IID/diffs" + rules: + - if: $CI_PIPELINE_SOURCE=="merge_request_event" && $CI_COMMIT_BRANCH =~ "main" + script: + - echo "merge request" + - | + curl -X POST -H "Content-Type: application/json" \ + -d "{\"msg_type\":\"text\",\"content\":{\"text\":\"项目:${CI_PROJECT_NAME}\\n提交人:${GITLAB_USER_NAME}\\n提交信息:${CI_MERGE_REQUEST_TITLE}\\n合并分支信息:${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME} -> ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}\\n差异性地址:${DIFF_URL}\\n请及时review代码\"}}" \ + https://open.feishu.cn/open-apis/bot/v2/hook/1c334752-2874-41a1-8f1b-3060f2d46b6c + +prebuild: + stage: prefix + rules: + - if: $CI_COMMIT_BRANCH == "main" + script: + - echo "prebuild" + - chmod +x ./scripts/prefix.sh + - ./scripts/prefix.sh + +builder: + stage: build + rules: + - if: $CI_COMMIT_BRANCH == "main" + script: + - set -e + - | + if [ ! -d "../artifacts" ]; then + mkdir -p ../artifacts + fi + if [ -d "../artifacts/dist" ]; then + cp -r ../artifacts/dist frontend/dist + fi + - | + if [ -n "$(git diff --name-status HEAD~1 HEAD -- frontend)" ]; then + ./scripts/build.sh $BUILD_DIR ${VERSION} all "" + else + ./scripts/build.sh $BUILD_DIR ${VERSION} + fi + if [ -d "frontend/dist" ]; then + echo "copy frontend/dist to artifacts/dist" + rm -fr ../artifacts/dist + cp -r frontend/dist ../artifacts/dist + fi + cp $BUILD_DIR/${APP_PRE}_linux_amd64.tar.gz ${SAVE_DIR} + +deployer: + stage: deploy + rules: + - if: $CI_COMMIT_BRANCH == "main" + variables: + APIPARK_GUEST_MODE: allow + APIPARK_GUEST_ID: dklejrfbhjqwdh + script: + - cd ${SAVE_DIR};mkdir -p ${APP_PRE};tar -zxvf ${APP_PRE}_linux_amd64.tar.gz -C ${APP_PRE};cd ${APP_PRE};./install.sh ${SAVE_DIR};./run.sh restart;cd ${SAVE_DIR} && ./clean.sh ${APP_PRE} + when: on_success +success: + stage: webhook + rules: + - if: $CI_COMMIT_BRANCH == "main" + script: + - | + curl -X POST -H "Content-Type: application/json" \ + -d "{\"msg_type\":\"text\",\"content\":{\"text\":\"最近一次提交:${CI_COMMIT_TITLE}\\n提交人:${GITLAB_USER_NAME}\\n项目:${CI_PROJECT_NAME}\\n环境:${DEPLOY_DESC}\\n更新部署完成.\\n访问地址:${VIEW_ADDR}\\n工作流地址:${CI_PIPELINE_URL}\"}}" \ + https://open.feishu.cn/open-apis/bot/v2/hook/c3672932-4dfa-4989-8023-0128bae59338 + when: on_success +failure: + stage: webhook + rules: + - if: $CI_COMMIT_BRANCH == "main" + script: + - | + curl -X POST -H "Content-Type: application/json" \ + -d "{\"msg_type\":\"text\",\"content\":{\"text\":\"最近一次提交:${CI_COMMIT_TITLE}\\n提交人:${GITLAB_USER_NAME}\\n项目:${CI_PROJECT_NAME}\\n环境:${DEPLOY_DESC}\\n更新部署失败,请及时到gitlab上查看\\n工作流地址:${CI_PIPELINE_URL}\"}}" \ + https://open.feishu.cn/open-apis/bot/v2/hook/c3672932-4dfa-4989-8023-0128bae59338 + when: on_failure diff --git a/README.md b/README.md index 6c52c63a..4ab6e887 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ APIPark uses the Apache 2.0 License. For more details, please refer to the LICEN For enterprise-level features and professional technical support, contact our pre-sales experts for personalized demos, customized solutions, and pricing. - Website: https://apipark.com -- Email: dev@apipark.com +- Email: contact@apipark.com
diff --git a/ai-provider/model-runtime/loader.go b/ai-provider/model-runtime/loader.go index 5288299f..5320ff0e 100644 --- a/ai-provider/model-runtime/loader.go +++ b/ai-provider/model-runtime/loader.go @@ -40,7 +40,14 @@ func (c *Config) Check(cfg string) error { } func (c *Config) GenConfig(target string, origin string) (string, error) { + if target == "" { + target = "{}" + } + if origin == "" { + origin = "{}" + } var targetData map[string]interface{} + err := json.Unmarshal([]byte(target), &targetData) if err != nil { return "", err diff --git a/app/ai-event-handler/.gitignore b/app/ai-event-handler/.gitignore new file mode 100644 index 00000000..a0ec5967 --- /dev/null +++ b/app/ai-event-handler/.gitignore @@ -0,0 +1 @@ +/config.yml diff --git a/app/ai-event-handler/main.go b/app/ai-event-handler/main.go new file mode 100644 index 00000000..f768a7d7 --- /dev/null +++ b/app/ai-event-handler/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "os/signal" + "syscall" + + "github.com/eolinker/go-common/autowire" + nsq "github.com/nsqio/go-nsq" + + "github.com/eolinker/go-common/cftool" + + _ "github.com/eolinker/go-common/store/store_mysql" + _ "github.com/go-sql-driver/mysql" +) + +var ( + version string + confPath string +) + +func init() { + flag.StringVar(&confPath, "c", "config.yml", "`config` file path for server ") +} + +type ServerConfig struct { + Port int `yaml:"port"` +} + +func main() { + // 1. 连接 MySQL 数据库 + cftool.Register[ServerConfig](fmt.Sprintf("root:%s", confPath)) + cftool.ReadFile(confPath) + + handler := &NSQHandler{} + autowire.Autowired(handler) + err := autowire.CheckComplete() + if err != nil { + log.Fatal("check autowired:", err) + return + } + // 2. 创建 NSQ 消费者 + config := nsq.NewConfig() + hostname, err := os.Hostname() + if err != nil { + log.Fatalf("Failed to get hostname: %v", err) + return + } + nsqConfig := handler.nsqConfig + consumer, err := nsq.NewConsumer(fmt.Sprintf("%s_ai_event", nsqConfig.TopicPrefix), hostname, config) + if err != nil { + log.Fatalf("Failed to create NSQ consumer: %v", err) + } + + consumer.AddHandler(handler) + + // 4. 连接到 NSQ + //nsqAddress := "172.18.166.219:9150" // NSQ 地址 + err = consumer.ConnectToNSQD(nsqConfig.Addr) + if err != nil { + log.Fatalf("Failed to connect to NSQ: %v", err) + } + log.Println("Connected to NSQ") + + // 5. 捕获系统信号,优雅关闭 + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + <-sigChan + + // 优雅停止消费者 + consumer.Stop() + <-consumer.StopChan + log.Println("NSQ Consumer stopped") +} diff --git a/app/ai-event-handler/nsq.go b/app/ai-event-handler/nsq.go new file mode 100644 index 00000000..3f891490 --- /dev/null +++ b/app/ai-event-handler/nsq.go @@ -0,0 +1,167 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "strings" + "time" + + "github.com/eolinker/go-common/cftool" + + ai_dto "github.com/APIParkLab/APIPark/module/ai/dto" + + "github.com/eolinker/go-common/store" + + "github.com/APIParkLab/APIPark/service/ai" + + ai_key "github.com/APIParkLab/APIPark/service/ai-key" + + nsq "github.com/nsqio/go-nsq" + + ai_api "github.com/APIParkLab/APIPark/service/ai-api" +) + +func init() { + cftool.Register[NSQConfig]("nsq") +} + +type NSQConfig struct { + Addr string `json:"addr" yaml:"addr"` + TopicPrefix string `json:"topic_prefix" yaml:"topic_prefix"` +} + +// 定义 NSQ 消息结构 +type AIProviderStatus struct { + Provider string `json:"provider"` + Model string `json:"model"` + Key string `json:"key"` + Status string `json:"status"` +} + +type AIInfo struct { + Model string `json:"ai_model"` + Cost interface{} `json:"ai_model_cost"` + InputToken interface{} `json:"ai_model_input_token"` + OutputToken interface{} `json:"ai_model_output_token"` + TotalToken interface{} `json:"ai_model_total_token"` + Provider string `json:"ai_provider"` + ProviderStats []AIProviderStatus `json:"ai_provider_statuses"` +} + +type NSQMessage struct { + AI AIInfo `json:"ai"` + API string `json:"api"` + Provider string `json:"provider"` + RequestID string `json:"request_id"` + TimeISO8601 string `json:"time_iso8601"` +} + +// NSQHandler 处理 NSQ 消息并写入 MySQL +type NSQHandler struct { + apiUseService ai_api.IAPIUseService `autowired:""` + aiKeyService ai_key.IKeyService `autowired:""` + aiService ai.IProviderService `autowired:""` + transaction store.ITransaction `autowired:""` + nsqConfig *NSQConfig `autowired:""` + ctx context.Context +} + +func convertInt(value interface{}) int { + switch v := value.(type) { + case int: + return v + case float64: + return int(v) + default: + return 0 + } +} + +func genAIKey(key string, provider string) string { + keys := strings.Split(key, "@") + return strings.TrimSuffix(keys[0], fmt.Sprintf("-%s", provider)) +} + +// HandleMessage 处理从 NSQ 读取的消息 +func (h *NSQHandler) HandleMessage(message *nsq.Message) error { + log.Printf("Received message: %s", string(message.Body)) + + // 解析消息为结构体 + var data NSQMessage + err := json.Unmarshal(message.Body, &data) + if err != nil { + log.Printf("Failed to unmarshal message: %v", err) + return nil + } + + // 将时间字符串转换为 time.Time + timestamp, err := time.Parse(time.RFC3339, data.TimeISO8601) + if err != nil { + log.Printf("Failed to parse timestamp: %v", err) + return nil + } + + day := time.Date(timestamp.Year(), timestamp.Month(), timestamp.Day(), 0, 0, 0, 0, timestamp.Location()) + hour := time.Date(timestamp.Year(), timestamp.Month(), timestamp.Day(), timestamp.Hour(), 0, 0, 0, timestamp.Location()) + minute := time.Date(timestamp.Year(), timestamp.Month(), timestamp.Day(), timestamp.Hour(), timestamp.Minute(), 0, 0, timestamp.Location()) + return h.transaction.Transaction(context.Background(), func(ctx context.Context) error { + finalStatus := &AIProviderStatus{} + for _, s := range data.AI.ProviderStats { + status := ToKeyStatus(s.Status).Int() + key := genAIKey(s.Key, s.Provider) + err = h.aiKeyService.Save(ctx, key, &ai_key.Edit{ + Status: &status, + }) + if err != nil { + log.Printf("Failed to save AI key: %v", err) + return nil + } + if s.Provider != data.AI.Provider { + + pStatus := ai_dto.ProviderAbnormal.Int() + err = h.aiService.Save(ctx, s.Provider, &ai.SetProvider{ + Status: &pStatus, + }) + } else { + pStatus := ai_dto.ProviderEnabled.Int() + err = h.aiService.Save(ctx, s.Provider, &ai.SetProvider{ + Status: &pStatus, + }) + } + finalStatus = &s + } + if finalStatus != nil { + //keys := strings.Split(finalStatus.Key, "@") + key := genAIKey(finalStatus.Key, finalStatus.Provider) + err = h.aiKeyService.IncrUseToken(ctx, key, convertInt(data.AI.TotalToken)) + if err != nil { + log.Printf("Failed to increment AI key token: %v", err) + return nil + } + } + + // 调用 AI API 接口 + err = h.apiUseService.Incr(context.Background(), &ai_api.IncrAPIUse{ + API: data.API, + Service: data.Provider, + Provider: data.AI.Provider, + Model: data.AI.Model, + Day: day.Unix(), + Hour: hour.Unix(), + Minute: minute.Unix(), + InputToken: convertInt(data.AI.InputToken), + OutputToken: convertInt(data.AI.OutputToken), + TotalToken: convertInt(data.AI.TotalToken), + }) + if err != nil { + log.Printf("Failed to call AI API: %v", err) + return nil + } + + log.Printf("Message processed and saved to MySQL: %+v", data) + return nil + }) + +} diff --git a/app/ai-event-handler/status.go b/app/ai-event-handler/status.go new file mode 100644 index 00000000..9b1ff61d --- /dev/null +++ b/app/ai-event-handler/status.go @@ -0,0 +1,34 @@ +package main + +import ai_key_dto "github.com/APIParkLab/APIPark/module/ai-key/dto" + +var ( + StatusNormal = "normal" + StatusInvalidRequest = "invalid request" + StatusQuotaExhausted = "quota exhausted" + StatusExpired = "expired" + StatusExceeded = "exceeded" + StatusInvalid = "invalid" + StatusTimeout = "timeout" +) + +func ToKeyStatus(status string) ai_key_dto.KeyStatus { + switch status { + case StatusNormal: + return ai_key_dto.KeyNormal + case StatusInvalidRequest: + return ai_key_dto.KeyNormal + case StatusQuotaExhausted: + return ai_key_dto.KeyExceed + case StatusExpired: + return ai_key_dto.KeyExpired + case StatusExceeded: + return ai_key_dto.KeyNormal + case StatusInvalid: + return ai_key_dto.KeyError + case StatusTimeout: + return ai_key_dto.KeyError + default: + return ai_key_dto.KeyNormal + } +} diff --git a/controller/ai-api/iml.go b/controller/ai-api/iml.go index fc59cc5f..d66a77a3 100644 --- a/controller/ai-api/iml.go +++ b/controller/ai-api/iml.go @@ -2,7 +2,6 @@ package ai_api import ( "context" - "fmt" "net/http" "github.com/APIParkLab/APIPark/model/plugin_model" @@ -52,7 +51,7 @@ func (i *imlAPIController) Create(ctx *gin.Context, serviceId string, input *ai_ plugins["ai_formatter"] = api.PluginSetting{ Config: plugin_model.ConfigType{ "model": input.AiModel.Id, - "provider": fmt.Sprintf("%s@ai-provider", input.AiModel.Provider), + "provider": input.AiModel.Provider, "config": input.AiModel.Config, }, } @@ -73,8 +72,8 @@ func (i *imlAPIController) Create(ctx *gin.Context, serviceId string, input *ai_ Retry: input.Retry, Plugins: plugins, }, - Upstream: input.AiModel.Provider, - Disable: false, + //Upstream: input.AiModel.Provider, + Disable: false, }) return err @@ -101,16 +100,16 @@ func (i *imlAPIController) Edit(ctx *gin.Context, serviceId string, apiId string Retry: apiInfo.Proxy.Retry, Plugins: apiInfo.Proxy.Plugins, } - var upstream *string + //var upstream *string if input.AiModel != nil { proxy.Plugins["ai_formatter"] = api.PluginSetting{ Config: plugin_model.ConfigType{ "model": input.AiModel.Id, - "provider": fmt.Sprintf("%s@ai-provider", input.AiModel.Provider), + "provider": input.AiModel.Provider, "config": input.AiModel.Config, }, } - upstream = &input.AiModel.Provider + //upstream = &input.AiModel.Provider } if input.AiPrompt != nil { @@ -128,7 +127,7 @@ func (i *imlAPIController) Edit(ctx *gin.Context, serviceId string, apiId string Path: input.Path, Disable: input.Disable, Methods: &apiInfo.Methods, - Upstream: upstream, + //Upstream: upstream, }) if err != nil { return err diff --git a/controller/ai-key/controller.go b/controller/ai-key/controller.go new file mode 100644 index 00000000..09bd5938 --- /dev/null +++ b/controller/ai-key/controller.go @@ -0,0 +1,26 @@ +package ai_key + +import ( + "reflect" + + ai_key_dto "github.com/APIParkLab/APIPark/module/ai-key/dto" + "github.com/eolinker/go-common/autowire" + "github.com/gin-gonic/gin" +) + +type IKeyController interface { + Create(ctx *gin.Context, providerId string, input *ai_key_dto.Create) error + Edit(ctx *gin.Context, providerId string, id string, input *ai_key_dto.Edit) error + Delete(ctx *gin.Context, providerId string, id string) error + Get(ctx *gin.Context, providerId string, id string) (*ai_key_dto.Key, error) + List(ctx *gin.Context, providerId string, keyword string, page string, pageSize string, statuses string) ([]*ai_key_dto.Item, int64, error) + Enable(ctx *gin.Context, providerId string, id string) error + Disable(ctx *gin.Context, providerId string, id string) error + Sort(ctx *gin.Context, providerId string, input *ai_key_dto.Sort) error +} + +func init() { + autowire.Auto[IKeyController](func() reflect.Value { + return reflect.ValueOf(new(imlAIKeyController)) + }) +} diff --git a/controller/ai-key/iml.go b/controller/ai-key/iml.go new file mode 100644 index 00000000..bf431333 --- /dev/null +++ b/controller/ai-key/iml.go @@ -0,0 +1,69 @@ +package ai_key + +import ( + "encoding/json" + "strconv" + + ai_key "github.com/APIParkLab/APIPark/module/ai-key" + ai_key_dto "github.com/APIParkLab/APIPark/module/ai-key/dto" + "github.com/gin-gonic/gin" +) + +var _ IKeyController = &imlAIKeyController{} + +type imlAIKeyController struct { + module ai_key.IKeyModule `autowired:""` +} + +func (i *imlAIKeyController) Enable(ctx *gin.Context, providerId string, id string) error { + return i.module.UpdateKeyStatus(ctx, providerId, id, true) +} + +func (i *imlAIKeyController) Disable(ctx *gin.Context, providerId string, id string) error { + return i.module.UpdateKeyStatus(ctx, providerId, id, false) +} + +func (i *imlAIKeyController) Create(ctx *gin.Context, providerId string, input *ai_key_dto.Create) error { + return i.module.Create(ctx, providerId, input) +} + +func (i *imlAIKeyController) Edit(ctx *gin.Context, providerId string, id string, input *ai_key_dto.Edit) error { + return i.module.Edit(ctx, providerId, id, input) +} + +func (i *imlAIKeyController) Delete(ctx *gin.Context, providerId string, id string) error { + return i.module.Delete(ctx, providerId, id) +} + +func (i *imlAIKeyController) Get(ctx *gin.Context, providerId string, id string) (*ai_key_dto.Key, error) { + return i.module.Get(ctx, providerId, id) +} + +func (i *imlAIKeyController) List(ctx *gin.Context, providerId string, keyword string, page string, pageSize string, statuses string) ([]*ai_key_dto.Item, int64, error) { + p, err := strconv.Atoi(page) + if err != nil { + if page != "" { + return nil, 0, err + } + p = 1 + } + ps, err := strconv.Atoi(pageSize) + if err != nil { + if pageSize != "" { + return nil, 0, err + } + ps = 20 + } + ss := make([]string, 0) + if statuses != "" { + err = json.Unmarshal([]byte(statuses), &ss) + if err != nil { + return nil, 0, err + } + } + return i.module.List(ctx, providerId, keyword, p, ps, ss) +} + +func (i *imlAIKeyController) Sort(ctx *gin.Context, providerId string, input *ai_key_dto.Sort) error { + return i.module.Sort(ctx, providerId, input) +} diff --git a/controller/ai/controller.go b/controller/ai/controller.go index 9a35bb77..581425d9 100644 --- a/controller/ai/controller.go +++ b/controller/ai/controller.go @@ -1,25 +1,37 @@ package ai import ( + "reflect" + ai_dto "github.com/APIParkLab/APIPark/module/ai/dto" "github.com/eolinker/go-common/autowire" "github.com/gin-gonic/gin" - "reflect" ) type IProviderController interface { - Providers(ctx *gin.Context) ([]*ai_dto.ProviderItem, error) + ConfiguredProviders(ctx *gin.Context) ([]*ai_dto.ConfiguredProviderItem, *ai_dto.BackupProvider, error) + UnConfiguredProviders(ctx *gin.Context) ([]*ai_dto.ProviderItem, error) SimpleProviders(ctx *gin.Context) ([]*ai_dto.SimpleProviderItem, error) + SimpleConfiguredProviders(ctx *gin.Context) ([]*ai_dto.SimpleProviderItem, *ai_dto.BackupProvider, error) Provider(ctx *gin.Context, id string) (*ai_dto.Provider, error) + SimpleProvider(ctx *gin.Context, id string) (*ai_dto.SimpleProvider, error) LLMs(ctx *gin.Context, driver string) ([]*ai_dto.LLMItem, *ai_dto.ProviderItem, error) Enable(ctx *gin.Context, id string) error Disable(ctx *gin.Context, id string) error UpdateProviderConfig(ctx *gin.Context, id string, input *ai_dto.UpdateConfig) error UpdateProviderDefaultLLM(ctx *gin.Context, id string, input *ai_dto.UpdateLLM) error + Sort(ctx *gin.Context, input *ai_dto.Sort) error +} + +type IStatisticController interface { + APIs(ctx *gin.Context, keyword string, providerId string, start string, end string, page string, pageSize string, sortCondition string, asc string, models string, services string) ([]*ai_dto.APIItem, *ai_dto.Condition, int64, error) } func init() { autowire.Auto[IProviderController](func() reflect.Value { return reflect.ValueOf(&imlProviderController{}) }) + autowire.Auto[IStatisticController](func() reflect.Value { + return reflect.ValueOf(&imlStatisticController{}) + }) } diff --git a/controller/ai/iml.go b/controller/ai/iml.go index e16e70bd..78b51087 100644 --- a/controller/ai/iml.go +++ b/controller/ai/iml.go @@ -1,6 +1,9 @@ package ai import ( + "encoding/json" + "strconv" + "github.com/APIParkLab/APIPark/module/ai" ai_dto "github.com/APIParkLab/APIPark/module/ai/dto" "github.com/gin-gonic/gin" @@ -14,28 +17,46 @@ type imlProviderController struct { module ai.IProviderModule `autowired:""` } +func (i *imlProviderController) Sort(ctx *gin.Context, input *ai_dto.Sort) error { + return i.module.Sort(ctx, input) +} + +func (i *imlProviderController) ConfiguredProviders(ctx *gin.Context) ([]*ai_dto.ConfiguredProviderItem, *ai_dto.BackupProvider, error) { + return i.module.ConfiguredProviders(ctx) +} + +func (i *imlProviderController) UnConfiguredProviders(ctx *gin.Context) ([]*ai_dto.ProviderItem, error) { + return i.module.UnConfiguredProviders(ctx) +} + func (i *imlProviderController) SimpleProviders(ctx *gin.Context) ([]*ai_dto.SimpleProviderItem, error) { return i.module.SimpleProviders(ctx) } -func (i *imlProviderController) Providers(ctx *gin.Context) ([]*ai_dto.ProviderItem, error) { - return i.module.Providers(ctx) +func (i *imlProviderController) SimpleConfiguredProviders(ctx *gin.Context) ([]*ai_dto.SimpleProviderItem, *ai_dto.BackupProvider, error) { + return i.module.SimpleConfiguredProviders(ctx) } func (i *imlProviderController) Provider(ctx *gin.Context, id string) (*ai_dto.Provider, error) { return i.module.Provider(ctx, id) } +func (i *imlProviderController) SimpleProvider(ctx *gin.Context, id string) (*ai_dto.SimpleProvider, error) { + return i.module.SimpleProvider(ctx, id) +} + func (i *imlProviderController) LLMs(ctx *gin.Context, driver string) ([]*ai_dto.LLMItem, *ai_dto.ProviderItem, error) { return i.module.LLMs(ctx, driver) } func (i *imlProviderController) Enable(ctx *gin.Context, id string) error { - return i.module.UpdateProviderStatus(ctx, id, true) + //return i.module.UpdateProviderStatus(ctx, id, true) + return nil } func (i *imlProviderController) Disable(ctx *gin.Context, id string) error { - return i.module.UpdateProviderStatus(ctx, id, false) + //return i.module.UpdateProviderStatus(ctx, id, false) + return nil } func (i *imlProviderController) UpdateProviderConfig(ctx *gin.Context, id string, input *ai_dto.UpdateConfig) error { @@ -43,5 +64,51 @@ func (i *imlProviderController) UpdateProviderConfig(ctx *gin.Context, id string } func (i *imlProviderController) UpdateProviderDefaultLLM(ctx *gin.Context, id string, input *ai_dto.UpdateLLM) error { - return i.module.UpdateProviderDefaultLLM(ctx, id, input) + //return i.module.UpdateProviderDefaultLLM(ctx, id, input) + return nil +} + +var _ IStatisticController = (*imlStatisticController)(nil) + +type imlStatisticController struct { + module ai.IAIAPIModule `autowired:""` +} + +func (i *imlStatisticController) APIs(ctx *gin.Context, keyword string, providerId string, start string, end string, page string, pageSize string, sortCondition string, asc string, models string, services string) ([]*ai_dto.APIItem, *ai_dto.Condition, int64, error) { + s, err := strconv.ParseInt(start, 10, 64) + if err != nil { + return nil, nil, 0, err + } + + e, err := strconv.ParseInt(end, 10, 64) + if err != nil { + return nil, nil, 0, err + } + + p, err := strconv.Atoi(page) + if err != nil { + if page != "" { + return nil, nil, 0, err + } + p = 1 + } + + ps, err := strconv.Atoi(pageSize) + if err != nil { + if pageSize != "" { + return nil, nil, 0, err + } + ps = 20 + } + ms := make([]string, 0) + if models != "" { + json.Unmarshal([]byte(models), &ms) + ms = append(ms, models) + } + ss := make([]string, 0) + if services != "" { + json.Unmarshal([]byte(services), &ss) + ss = append(ss, services) + } + return i.module.APIs(ctx, keyword, providerId, s, e, p, ps, sortCondition, asc == "true", ms, ss) } diff --git a/controller/service/iml.go b/controller/service/iml.go index 7227e6b7..f75d66bf 100644 --- a/controller/service/iml.go +++ b/controller/service/iml.go @@ -318,8 +318,8 @@ func (i *imlServiceController) Get(ctx *gin.Context, id string) (*service_dto.Se return i.module.Get(ctx, id) } -func (i *imlServiceController) Search(ctx *gin.Context, teamID string, keyword string) ([]*service_dto.ServiceItem, error) { - return i.module.Search(ctx, teamID, keyword) +func (i *imlServiceController) Search(ctx *gin.Context, teamIDs string, keyword string) ([]*service_dto.ServiceItem, error) { + return i.module.Search(ctx, teamIDs, keyword) } func (i *imlServiceController) Create(ctx *gin.Context, teamID string, input *service_dto.CreateService) (*service_dto.Service, error) { diff --git a/controller/service/service.go b/controller/service/service.go index 4e2a553e..c46ad21f 100644 --- a/controller/service/service.go +++ b/controller/service/service.go @@ -15,7 +15,7 @@ type IServiceController interface { Get(ctx *gin.Context, id string) (*service_dto.Service, error) // SearchMyServices 搜索服务 SearchMyServices(ctx *gin.Context, teamID string, keyword string) ([]*service_dto.ServiceItem, error) - Search(ctx *gin.Context, teamID string, keyword string) ([]*service_dto.ServiceItem, error) + Search(ctx *gin.Context, teamIDs string, keyword string) ([]*service_dto.ServiceItem, error) // Create 创建 Create(ctx *gin.Context, teamID string, input *service_dto.CreateService) (*service_dto.Service, error) // Edit 编辑 diff --git a/controller/system/iml.go b/controller/system/iml.go index 1b9c0434..f54e05b4 100644 --- a/controller/system/iml.go +++ b/controller/system/iml.go @@ -437,7 +437,7 @@ func (i *imlInitController) createAIService(ctx context.Context, teamID string, plugins["ai_formatter"] = api.PluginSetting{ Config: plugin_model.ConfigType{ "model": aiModel.Id, - "provider": fmt.Sprintf("%s@ai-provider", info.Provider.Id), + "provider": info.Provider.Id, "config": aiModel.Config, }, } @@ -457,8 +457,8 @@ func (i *imlInitController) createAIService(ctx context.Context, teamID string, Retry: retry, Plugins: plugins, }, - Disable: false, - Upstream: info.Provider.Id, + Disable: false, + //Upstream: info.Provider.Id, }) if err != nil { return err diff --git a/frontend/packages/common/src/components/aoplatform/LanguageSetting.tsx b/frontend/packages/common/src/components/aoplatform/LanguageSetting.tsx index 90a20efa..946a712c 100644 --- a/frontend/packages/common/src/components/aoplatform/LanguageSetting.tsx +++ b/frontend/packages/common/src/components/aoplatform/LanguageSetting.tsx @@ -4,48 +4,48 @@ import { Icon } from '@iconify/react/dist/iconify.js' import { Button, Dropdown } from 'antd' import { memo, useEffect, useMemo } from 'react' +const LanguageItems = [ + { + key: 'en-US', + label: ( + + ), + title: 'English' + }, + { + key: 'ja-JP', + label: ( + + ), + title: '日本語' + }, + { + key: 'zh-TW', + label: ( + + ), + title: '繁體中文' + }, + { + key: 'zh-CN', + label: ( + + ), + title: '简体中文' + } +] const LanguageSetting = ({ mode = 'light' }: { mode?: 'dark' | 'light' }) => { const { dispatch, state } = useGlobalContext() - const items = [ - { - key: 'en-US', - label: ( - - ), - title: 'English' - }, - { - key: 'ja-JP', - label: ( - - ), - title: '日本語' - }, - { - key: 'zh-TW', - label: ( - - ), - title: '繁體中文' - }, - { - key: 'zh-CN', - label: ( - - ), - title: '简体中文' - } - ] - const langLabel = useMemo(() => items.find((item) => item?.key === state.language)?.title, [state.language]) + const langLabel = useMemo(() => LanguageItems.find((item) => item?.key === state.language)?.title, [state.language]) useEffect(() => { const savedLang = i18n.language || sessionStorage.getItem('i18nextLng') @@ -53,7 +53,8 @@ const LanguageSetting = ({ mode = 'light' }: { mode?: 'dark' | 'light' }) => { dispatch({ type: 'UPDATE_LANGUAGE', language: savedLang }) } else if (!savedLang) { const browserLang = navigator.language - const supportedLang = items.find((item) => item.key === browserLang) ? browserLang : 'zh-CN' + const supportedLang = LanguageItems.find((item) => item.key === browserLang) ? browserLang : 'zh-CN' + if (state.language === supportedLang) return dispatch({ type: 'UPDATE_LANGUAGE', language: supportedLang }) i18n.changeLanguage(supportedLang) } @@ -63,7 +64,7 @@ const LanguageSetting = ({ mode = 'light' }: { mode?: 'dark' | 'light' }) => { { const { key } = e diff --git a/frontend/packages/common/src/components/aoplatform/WithPermission.tsx b/frontend/packages/common/src/components/aoplatform/WithPermission.tsx index 719cd5bd..9d1740af 100644 --- a/frontend/packages/common/src/components/aoplatform/WithPermission.tsx +++ b/frontend/packages/common/src/components/aoplatform/WithPermission.tsx @@ -1,8 +1,8 @@ +import { PERMISSION_DEFINITION } from '@common/const/permissions' +import { $t } from '@common/locales' import { Button, Tooltip, Upload } from 'antd' import { ReactElement, cloneElement, useEffect, useMemo, useState } from 'react' import { useGlobalContext } from '../../contexts/GlobalStateContext' -import { PERMISSION_DEFINITION } from '@common/const/permissions' -import { $t } from '@common/locales' type WithPermissionProps = { access?: string | string[] diff --git a/frontend/packages/common/src/contexts/GlobalStateContext.tsx b/frontend/packages/common/src/contexts/GlobalStateContext.tsx index da730fa6..31fbb1d4 100644 --- a/frontend/packages/common/src/contexts/GlobalStateContext.tsx +++ b/frontend/packages/common/src/contexts/GlobalStateContext.tsx @@ -342,7 +342,7 @@ export const GlobalProvider: FC<{ children: ReactNode }> = ({ children }) => { updateDate: '2024-07-01', powered: 'Powered by https://apipark.com', mainPage: '/guide/page', - language: 'en-US', + language: sessionStorage.getItem('i18nextLng') || 'en-US', pluginsLoaded: false }) const [accessData, setAccessData] = useState>(new Map()) @@ -510,7 +510,7 @@ export const useGlobalContext = () => { updateDate: '', powered: '', mainPage: '', - language: 'en-US', + language: sessionStorage.getItem('i18nextLng') || 'en-US', pluginsLoaded: false }, dispatch: () => {}, diff --git a/frontend/packages/common/src/locales/index.ts b/frontend/packages/common/src/locales/index.ts index 8a98b857..5da94e8a 100644 --- a/frontend/packages/common/src/locales/index.ts +++ b/frontend/packages/common/src/locales/index.ts @@ -1,5 +1,5 @@ import i18n from 'i18next' -import { initReactI18next } from 'react-i18next' +import { initReactI18next, useTranslation } from 'react-i18next' // i18next-browser-languagedetector插件 这是一个 i18next 语言检测插件,用于检测浏览器中的用户语言, import crc32 from 'crc/crc32' import LanguageDetector from 'i18next-browser-languagedetector' @@ -39,23 +39,22 @@ i18n .init({ // 初始化 resources, // 本地多语言数据 - // fallbackLng: config.lang, // 默认当前环境的语言 + supportedLngs: ['zh-CN', 'en-US', 'zh-TW', 'ja-JP'], detection: { caches: ['localStorage', 'sessionStorage', 'cookie'] } }) // --------这里是i18next-scanner新增的配置------------- +// 用于非 React 组件中的翻译 export const $t = (key: string, params?: any[]): string => { - const hashKey = `K${crc32(key).toString(16)}` // 将中文转换成crc32格式去匹配对应的json语言包 + // 将中文转换成crc32格式去匹配对应的json语言包 + const hashKey = `K${crc32(key).toString(16)}` let words = i18n.t(hashKey) - // const { t } = useTranslation(); // 通过hooks - // let words = t(hashKey); if (words === hashKey) { words = key } - // 配置传递参数的场景, 目前仅支持数组,可在此拓展 if (Array.isArray(params)) { const reg = /\((\d)\)/g words = words.replace(reg, (a: string, b: number) => { @@ -65,4 +64,25 @@ export const $t = (key: string, params?: any[]): string => { return words } +// 用于 React 组件中的翻译 +export const useI18n = () => { + const { t } = useTranslation() + + return (key: string, params?: any[]): string => { + const hashKey = `K${crc32(key).toString(16)}` + let words = t(hashKey) + if (words === hashKey) { + words = key + } + + if (Array.isArray(params)) { + const reg = /\((\d)\)/g + words = words.replace(reg, (a: string, b: number) => { + return params[b] + }) + } + return words + } +} + export default i18n diff --git a/frontend/packages/common/src/utils/permission.ts b/frontend/packages/common/src/utils/permission.ts index f8317c28..d5517d44 100644 --- a/frontend/packages/common/src/utils/permission.ts +++ b/frontend/packages/common/src/utils/permission.ts @@ -18,7 +18,11 @@ export const checkAccess: (access: AccessDataType, accessData: Map 0 ? hasIntersection(neededBackendAccessArr, accessSet) : false + if (!accessSet!.size) { + return false + } + const hasAccess = hasIntersection(neededBackendAccessArr, accessSet) + return hasAccess } const hasIntersection = (arr1: string[], set1: Set) => { diff --git a/frontend/packages/core/src/App.tsx b/frontend/packages/core/src/App.tsx index aa5b4c40..cb98a151 100644 --- a/frontend/packages/core/src/App.tsx +++ b/frontend/packages/core/src/App.tsx @@ -151,15 +151,15 @@ function App() { form={{ validateMessages }} > - - - + + + - - - + + + diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx index 30694d5a..aa7034d2 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx @@ -1,5 +1,5 @@ import InsidePage from '@common/components/aoplatform/InsidePage' -import { $t } from '@common/locales' +import { useI18n } from '@common/locales' import { Tabs } from 'antd' import { useEffect, useState } from 'react' import { useSearchParams } from 'react-router-dom' @@ -10,6 +10,7 @@ import { AiSettingProvider } from './contexts/AiSettingContext' const CONTENT_STYLE = { height: 'calc(-300px + 100vh)' } as const const AiSettingContent = () => { + const $t = useI18n() const [searchParams, setSearchParams] = useSearchParams() const [activeKey, setActiveKey] = useState(searchParams.get('status') === 'unconfigure' ? 'config' : 'flow') diff --git a/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx b/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx index ede8936b..6fdbf0cc 100644 --- a/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx +++ b/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx @@ -65,6 +65,11 @@ export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) = {defaultLlm} + {status !== 'enabled' && alternativeModel && ( +
+ {$t('关联 API 已转用')} {alternativeModel.name}/{alternativeModel.defaultLlm} +
+ )} {status !== 'enabled' && alternativeModel && (
diff --git a/frontend/packages/core/src/pages/common/CommonPage.tsx b/frontend/packages/core/src/pages/common/CommonPage.tsx index e1e9ae86..7a2c62b4 100644 --- a/frontend/packages/core/src/pages/common/CommonPage.tsx +++ b/frontend/packages/core/src/pages/common/CommonPage.tsx @@ -1,33 +1,26 @@ -import InsidePage from "@common/components/aoplatform/InsidePage"; -import { $t } from "@common/locales"; -import ServiceCategory from "./ServiceCategory"; -import ApiRequestSetting from "./ApiRequestSetting"; -import { Row, Col } from "antd"; +import InsidePage from '@common/components/aoplatform/InsidePage' +import { useI18n } from '@common/locales' +import { Col, Row } from 'antd' +import ApiRequestSetting from './ApiRequestSetting' +import ServiceCategory from './ServiceCategory' -export default function CommonPage(){ - return ( - - - - - {$t('API 请求设置')} - - - - - - - - {$t('服务分类')} - - - - - - ) -} \ No newline at end of file +export default function CommonPage() { + const $t = useI18n() + + return ( + + + + {$t('API 请求设置')} + + + + + + {$t('服务分类')} + + + + + ) +} diff --git a/frontend/packages/core/src/pages/common/ServiceCategory.tsx b/frontend/packages/core/src/pages/common/ServiceCategory.tsx index 7ee2abcc..e3bf18bc 100644 --- a/frontend/packages/core/src/pages/common/ServiceCategory.tsx +++ b/frontend/packages/core/src/pages/common/ServiceCategory.tsx @@ -28,7 +28,7 @@ export default function ServiceCategory() { const { accessData } = useGlobalContext() const [loading, setLoading] = useState(false) - const onDrop: TreeProps['onDrop'] = info => { + const onDrop: TreeProps['onDrop'] = (info) => { const dropKey = info.node.key const dragKey = info.dragNode.key const dropPos = info.node.pos.split('-') @@ -59,7 +59,7 @@ export default function ServiceCategory() { if (!info.dropToGap) { // Drop on the content - loop(data, dropKey, item => { + loop(data, dropKey, (item) => { item.children = item.children || [] // where to insert. New item was inserted to the start of the array in this example, but can be anywhere item.children.unshift(dragObj) @@ -129,9 +129,9 @@ export default function ServiceCategory() { const treeData = useMemo(() => { setExpandedKeys([]) const loop = (data: CategorizesType[]): DataNode[] => - data?.map(item => { + data?.map((item) => { if (item.children) { - setExpandedKeys(prev => [...prev, item.id]) + setExpandedKeys((prev) => [...prev, item.id]) return { title: ( @@ -169,40 +169,22 @@ export default function ServiceCategory() { return !checkAccess(permission, accessData) } - const openModal = ( - type: 'addCate' | 'addChildCate' | 'renameCate' | 'delete', - entity?: CategorizesType - ) => { + const openModal = (type: 'addCate' | 'addChildCate' | 'renameCate' | 'delete', entity?: CategorizesType) => { let title: string = '' let content: string | React.ReactNode = '' switch (type) { - case 'addCate': + case 'addCate': { title = $t('添加分类') - content = ( - - ) + content = break + } case 'addChildCate': title = $t('添加子分类') - content = ( - - ) + content = break case 'renameCate': title = $t('重命名分类') - content = ( - - ) + content = break case 'delete': title = $t('删除') @@ -215,19 +197,19 @@ export default function ServiceCategory() { onOk: () => { switch (type) { case 'addCate': - return addRef.current?.save().then(res => { + return addRef.current?.save().then((res) => { if (res === true) getCategoryList() }) case 'addChildCate': - return addChildRef.current?.save().then(res => { + return addChildRef.current?.save().then((res) => { if (res === true) getCategoryList() }) case 'renameCate': - return renameRef.current?.save().then(res => { + return renameRef.current?.save().then((res) => { if (res === true) getCategoryList() }) case 'delete': - return deleteCate(entity!).then(res => { + return deleteCate(entity!).then((res) => { if (res === true) getCategoryList() }) } @@ -249,7 +231,7 @@ export default function ServiceCategory() { method: 'DELETE', eoParams: { catalogue: entity.id } }) - .then(response => { + .then((response) => { const { code, msg } = response if (code === STATUS_CODE.SUCCESS) { message.success(msg || $t(RESPONSE_TIPS.success)) @@ -259,14 +241,14 @@ export default function ServiceCategory() { reject(msg || $t(RESPONSE_TIPS.error)) } }) - .catch(errorInfo => reject(errorInfo)) + .catch((errorInfo) => reject(errorInfo)) }) } const sortCategories = (newData: CategorizesType[]) => { setLoading(true) fetchData>('catalogue/sort', { method: 'PUT', eoBody: newData }) - .then(response => { + .then((response) => { const { code, msg } = response if (code === STATUS_CODE.SUCCESS) { getCategoryList() @@ -288,7 +270,7 @@ export default function ServiceCategory() { fetchData>('catalogues', { method: 'GET' }) - .then(response => { + .then((response) => { const { code, data, msg } = response if (code === STATUS_CODE.SUCCESS) { setGData(data.catalogues) @@ -308,11 +290,7 @@ export default function ServiceCategory() { return (
- } - spinning={loading} - className="" - > + } spinning={loading} className=""> ((props, ref) => { - const { message } = App.useApp() - const [form] = Form.useForm() - const { type, entity } = props - const { fetchData } = useFetch() +export const ServiceHubCategoryConfig = forwardRef( + (props, ref) => { + const { message } = App.useApp() + const [form] = Form.useForm() + const { type, entity } = props + const { fetchData } = useFetch() - const save: () => Promise = () => { - const url: string = 'catalogue' - let method: string - switch (type) { - case 'addCate': - case 'addChildCate': - method = 'POST' - break - case 'renameCate': - method = 'PUT' - break - } - return new Promise((resolve, reject) => { - if (!url || !method) { - reject($t(RESPONSE_TIPS.error)) - return + const save: () => Promise = () => { + const url: string = 'catalogue' + let method: string + switch (type) { + case 'addCate': + case 'addChildCate': + method = 'POST' + break + case 'renameCate': + method = 'PUT' + break } - form - .validateFields() - .then(value => { - fetchData>(url, { - method, - eoBody: value, - eoParams: { ...(type === 'renameCate' ? { catalogue: value.id } : undefined) } - }) - .then(response => { - const { code, msg } = response - if (code === STATUS_CODE.SUCCESS) { - message.success(msg || $t(RESPONSE_TIPS.success)) - resolve(true) - } else { - message.error(msg || $t(RESPONSE_TIPS.error)) - reject(msg || $t(RESPONSE_TIPS.error)) - } + return new Promise((resolve, reject) => { + if (!url || !method) { + reject($t(RESPONSE_TIPS.error)) + return + } + form + .validateFields() + .then((value) => { + fetchData>(url, { + method, + eoBody: value, + eoParams: { ...(type === 'renameCate' ? { catalogue: value.id } : undefined) } }) - .catch(errorInfo => reject(errorInfo)) - }) - .catch(errorInfo => reject(errorInfo)) - }) - } - - useImperativeHandle(ref, () => ({ - save - })) - - useEffect(() => { - switch (type) { - case 'addCate': - form.setFieldsValue({}) - break - case 'addChildCate': - form.setFieldsValue({ parent: entity!.id }) - break - case 'renameCate': - form.setFieldsValue(entity) - break + .then((response) => { + const { code, msg } = response + if (code === STATUS_CODE.SUCCESS) { + message.success(msg || $t(RESPONSE_TIPS.success)) + resolve(true) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + reject(msg || $t(RESPONSE_TIPS.error)) + } + }) + .catch((errorInfo) => reject(errorInfo)) + }) + .catch((errorInfo) => reject(errorInfo)) + }) } - }, []) - return ( - ({ + save + })) + + useEffect(() => { + switch (type) { + case 'addCate': + form.setFieldsValue({}) + break + case 'addChildCate': + form.setFieldsValue({ parent: entity!.id }) + break + case 'renameCate': + form.setFieldsValue(entity) + break } - > -
- {type === 'renameCate' && ( - - label={$t('ID')} - name="id" - hidden - rules={[{ required: true, whitespace: true }]} - > - - - )} - {(type === 'addCate' || type === 'renameCate') && ( - - label={$t('分类名称')} - name="name" - rules={[{ required: true, whitespace: true }]} - > - - - )} + }, []) - {type === 'addChildCate' && ( - <> + return ( + + + {type === 'renameCate' && ( - label={$t('父分类 ID')} - name="parent" + label={$t('ID')} + name="id" hidden rules={[{ required: true, whitespace: true }]} > - + )} + {(type === 'addCate' || type === 'renameCate') && ( - label={$t('子分类名称')} + label={$t('分类名称')} name="name" rules={[{ required: true, whitespace: true }]} > - - )} - - - ) -}) + )} + + {type === 'addChildCate' && ( + <> + + label={$t('父分类 ID')} + name="parent" + hidden + rules={[{ required: true, whitespace: true }]} + > + + + + + label={$t('子分类名称')} + name="name" + rules={[{ required: true, whitespace: true }]} + > + + + + )} + +
+ ) + } +) diff --git a/frontend/packages/market/src/const/serviceHub/type.ts b/frontend/packages/market/src/const/serviceHub/type.ts index e973d88d..6e02a06b 100644 --- a/frontend/packages/market/src/const/serviceHub/type.ts +++ b/frontend/packages/market/src/const/serviceHub/type.ts @@ -1,7 +1,6 @@ -import { DefaultOptionType } from 'antd/es/select' import { EntityItem } from '@common/const/type' import { SubscribeEnum, SubscribeFromEnum } from '@core/const/system/const' -import WithPermission from '@common/components/aoplatform/WithPermission' +import { DefaultOptionType } from 'antd/es/select' export type ServiceBasicInfoType = { app: EntityItem @@ -37,7 +36,6 @@ export type ServiceHubCategoryConfigFieldType = { export type ServiceHubCategoryConfigProps = { type: 'addCate' | 'addChildCate' | 'renameCate' entity?: { [k: string]: unknown } - WithPermission: typeof WithPermission } export type ServiceHubCategoryConfigHandle = { diff --git a/frontend/packages/market/src/pages/serviceHub/management/ServiceHubManagement.tsx b/frontend/packages/market/src/pages/serviceHub/management/ServiceHubManagement.tsx index ef5f5ea2..8f0fe92d 100644 --- a/frontend/packages/market/src/pages/serviceHub/management/ServiceHubManagement.tsx +++ b/frontend/packages/market/src/pages/serviceHub/management/ServiceHubManagement.tsx @@ -5,7 +5,7 @@ import WithPermission from '@common/components/aoplatform/WithPermission' import { BasicResponse, DATA_SHOW_TYPE_OPTIONS, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' import { SimpleTeamItem } from '@common/const/type' import { useBreadcrumb } from '@common/contexts/BreadcrumbContext' -import { GlobalProvider, useGlobalContext } from '@common/contexts/GlobalStateContext' +import { useGlobalContext } from '@common/contexts/GlobalStateContext' import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' import { RouterParams } from '@core/components/aoplatform/RenderRoutes' @@ -158,28 +158,8 @@ export default function ServiceHubManagement() { switch (type) { case 'add': title = $t('添加消费者') - content = ( - - - - ) + content = break - // case 'edit':{ - // title='配置 Open Api' - // message.loading('正在加载数据') - // const {code,data,msg} = await fetchData>('external-app',{method:'GET',eoParams:{id:entity!.id}}) - // message.destroy() - // if(code === STATUS_CODE.SUCCESS){ - // content= - // }else{ - // message.error(msg || $t(RESPONSE_TIPS.error)) - // return - // } - // break;} - // case 'delete': - // title='删除' - // content='该数据删除后将无法找回,请确认是否删除?' - // break; } modal.confirm({ diff --git a/gateway/apinto/entity/router.go b/gateway/apinto/entity/router.go index 45406b28..664f0eb3 100644 --- a/gateway/apinto/entity/router.go +++ b/gateway/apinto/entity/router.go @@ -161,7 +161,7 @@ func ToRouter(r *gateway.ApiRelease, version string, matches map[string]string) labels = r.Labels } - return &Router{ + router := &Router{ BasicInfo: &BasicInfo{ ID: fmt.Sprintf("%s@router", r.ID), Name: r.ID, @@ -174,13 +174,16 @@ func ToRouter(r *gateway.ApiRelease, version string, matches map[string]string) Method: r.Methods, Location: r.Path, Rules: rules, - Service: fmt.Sprintf("%s@service", r.Service), Plugins: plugin, Retry: r.Retry, TimeOut: r.Timeout, Labels: labels, Protocols: []string{"http", "https"}, } + if r.Service != "" { + router.Service = fmt.Sprintf("%s@service", r.Service) + } + return router } // formatProxyPath 格式化转发路径上,用于转发重写插件正则替换 比如 请求路径 /path/{A}/{B} 原转发路径:/path/{B} 格式化后 新转发路径: /path/$2 diff --git a/gateway/apinto/plugin/apinto_plugin.yml b/gateway/apinto/plugin/apinto_plugin.yml index 4bd3765a..b3c3c6d8 100644 --- a/gateway/apinto/plugin/apinto_plugin.yml +++ b/gateway/apinto/plugin/apinto_plugin.yml @@ -28,7 +28,7 @@ b: "subscription_service:#{application}" response: status_code: 403 - content_typ: "text/plan" + content_type: "text/plan" charset: "utf-8" body: "Forbidden" diff --git a/gateway/profession.go b/gateway/profession.go index 606a4904..16956421 100644 --- a/gateway/profession.go +++ b/gateway/profession.go @@ -8,6 +8,7 @@ const ( ProfessionStrategy = "strategy" ProfessionService = "service" ProfessionAIProvider = "ai-provider" + ProfessionAIResource = "ai-resource" ) func RegisterDynamicResourceDriver(key string, worker Worker) { @@ -61,6 +62,14 @@ var dynamicResourceMap = map[string]Worker{ Profession: ProfessionOutput, Driver: "loki", }, + "ai-provider": { + Profession: ProfessionAIResource, + Driver: "ai-provider", + }, + "ai-key": { + Profession: ProfessionAIResource, + Driver: "ai-key", + }, } type Worker struct { diff --git a/go.mod b/go.mod index 1d48fab3..fb4396bf 100644 --- a/go.mod +++ b/go.mod @@ -7,12 +7,14 @@ go 1.21 require ( github.com/eolinker/ap-account v1.0.15 github.com/eolinker/eosc v0.18.3 - github.com/eolinker/go-common v1.1.1 + github.com/eolinker/go-common v1.1.4 github.com/gabriel-vasile/mimetype v1.4.4 github.com/getkin/kin-openapi v0.127.0 github.com/gin-gonic/gin v1.10.0 + github.com/go-sql-driver/mysql v1.7.0 github.com/google/uuid v1.6.0 github.com/influxdata/influxdb-client-go/v2 v2.14.0 + github.com/nsqio/go-nsq v1.1.0 github.com/urfave/cli v1.22.16 golang.org/x/crypto v0.24.0 gopkg.in/yaml.v3 v3.0.1 @@ -37,8 +39,8 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect - github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect @@ -81,4 +83,4 @@ require ( //replace github.com/eolinker/ap-account => ../aoaccount // -//replace github.com/eolinker/go-common => ../go-common +//replace github.com/eolinker/go-common => ../../eolinker/go-common diff --git a/go.sum b/go.sum index 9bc1954c..01c9b855 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/eolinker/ap-account v1.0.15 h1:n6DJeL6RHZ8eLlZUcY2U3H4d/GPaA5oelAx3R0 github.com/eolinker/ap-account v1.0.15/go.mod h1:zm/Ivs6waJ/M/nEszhpPmM6g50y/MKO+5eABFAdeD0g= github.com/eolinker/eosc v0.18.3 h1:3IK5HkAPnJRfLbQ0FR7kWsZr6Y/OiqqGazvN1q2BL5A= github.com/eolinker/eosc v0.18.3/go.mod h1:O9PQQXFCpB6fjHf+oFt/LN6EOAv779ItbMixMKCfTfk= -github.com/eolinker/go-common v1.1.1 h1:3WqqecGqcHDgpa8Ljp156c1uWeZKP1CKScdU+6sOfcc= -github.com/eolinker/go-common v1.1.1/go.mod h1:Kb/jENMN1mApnodvRgV4YwO9FJby1Jkt2EUjrBjvSX4= +github.com/eolinker/go-common v1.1.4 h1:U5AtQMr3RCudgeV6jcX5TemUGrTz8WqLu//KrZm3BzA= +github.com/eolinker/go-common v1.1.4/go.mod h1:Kb/jENMN1mApnodvRgV4YwO9FJby1Jkt2EUjrBjvSX4= github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY= @@ -64,6 +64,9 @@ 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/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -107,6 +110,8 @@ 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/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/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo= github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= diff --git a/init.go b/init.go index 74592a3a..af7b6ca9 100644 --- a/init.go +++ b/init.go @@ -5,6 +5,7 @@ import ( _ "github.com/APIParkLab/APIPark/frontend" _ "github.com/APIParkLab/APIPark/gateway/apinto" _ "github.com/APIParkLab/APIPark/plugins/core" + _ "github.com/APIParkLab/APIPark/plugins/openapi" _ "github.com/APIParkLab/APIPark/plugins/permit" _ "github.com/APIParkLab/APIPark/plugins/publish_flow" _ "github.com/APIParkLab/APIPark/resources/locale" diff --git a/module/ai-api/dto/input.go b/module/ai-api/dto/input.go index be94417e..fe71acac 100644 --- a/module/ai-api/dto/input.go +++ b/module/ai-api/dto/input.go @@ -5,7 +5,7 @@ type CreateAPI struct { Name string `json:"name"` Path string `json:"path"` Description string `json:"description"` - Disable bool `json:"disable"` + Disable bool `json:"disabled"` AiPrompt *AiPrompt `json:"ai_prompt"` AiModel *AiModel `json:"ai_model"` Timeout int `json:"timeout"` @@ -33,7 +33,7 @@ type EditAPI struct { Name *string `json:"name"` Path *string `json:"path"` Description *string `json:"description"` - Disable *bool `json:"disable"` + Disable *bool `json:"disabled"` AiPrompt *AiPrompt `json:"ai_prompt"` AiModel *AiModel `json:"ai_model"` Timeout *int `json:"timeout"` diff --git a/module/ai-api/dto/output.go b/module/ai-api/dto/output.go index 912bb878..a00c6690 100644 --- a/module/ai-api/dto/output.go +++ b/module/ai-api/dto/output.go @@ -9,7 +9,7 @@ type API struct { Name string `json:"name"` Path string `json:"path"` Description string `json:"description"` - Disable bool `json:"disable"` + Disable bool `json:"disabled"` AiPrompt *AiPrompt `json:"ai_prompt"` AiModel *AiModel `json:"ai_model"` Timeout int `json:"timeout"` @@ -21,7 +21,7 @@ type APIItem struct { Name string `json:"name"` RequestPath string `json:"request_path"` Description string `json:"description"` - Disable bool `json:"disable"` + Disable bool `json:"disabled"` Creator auto.Label `json:"creator" aolabel:"user"` Updater auto.Label `json:"updater" aolabel:"user"` CreateTime auto.TimeLabel `json:"create_time"` diff --git a/module/ai-api/iml.go b/module/ai-api/iml.go index 55a60fc2..12994aa2 100644 --- a/module/ai-api/iml.go +++ b/module/ai-api/iml.go @@ -8,6 +8,8 @@ import ( "net/http" "strings" + "github.com/eolinker/eosc/log" + model_runtime "github.com/APIParkLab/APIPark/ai-provider/model-runtime" ai_api_dto "github.com/APIParkLab/APIPark/module/ai-api/dto" @@ -110,10 +112,12 @@ func (i *imlAPIModule) Create(ctx context.Context, serviceId string, input *ai_a Name: input.Name, Service: serviceId, Path: input.Path, + Disable: input.Disable, Description: input.Description, Timeout: input.Timeout, Retry: input.Retry, Model: input.AiModel.Id, + Provider: input.AiModel.Provider, AdditionalConfig: map[string]interface{}{ "ai_prompt": input.AiPrompt, "ai_model": input.AiModel, @@ -148,8 +152,10 @@ func (i *imlAPIModule) Edit(ctx context.Context, serviceId string, apiId string, return err } var modelId *string + var providerId *string if input.AiModel != nil { modelId = &input.AiModel.Id + providerId = &input.AiModel.Provider } if input.AiPrompt != nil { apiInfo.AdditionalConfig["ai_prompt"] = input.AiPrompt @@ -164,7 +170,9 @@ func (i *imlAPIModule) Edit(ctx context.Context, serviceId string, apiId string, Timeout: input.Timeout, Retry: input.Retry, Model: modelId, + Provider: providerId, AdditionalConfig: &apiInfo.AdditionalConfig, + Disable: input.Disable, }) }) } @@ -304,3 +312,27 @@ func (i *imlAPIModule) Prefix(ctx context.Context, serviceId string) (string, er } return strings.TrimSuffix(pInfo.Prefix, "/"), nil } + +func (i *imlAPIModule) OnInit() { + ctx := context.Background() + list, err := i.aiAPIService.List(ctx) + if err != nil { + return + } + for _, item := range list { + if item.Provider == "" { + aiModel, err := ConvertStruct[ai_api_dto.AiModel](item.AdditionalConfig["ai_model"]) + if err != nil { + log.Errorf("convert ai model error:%v", err) + continue + } + err = i.aiAPIService.Save(ctx, item.ID, &ai_api.Edit{ + Provider: &aiModel.Provider, + }) + if err != nil { + log.Errorf("update ai api provider error:%v", err) + continue + } + } + } +} diff --git a/module/ai-api/module.go b/module/ai-api/module.go index 1119a2e0..b42714f5 100644 --- a/module/ai-api/module.go +++ b/module/ai-api/module.go @@ -2,9 +2,10 @@ package ai_api import ( "context" + "reflect" + ai_api_dto "github.com/APIParkLab/APIPark/module/ai-api/dto" "github.com/eolinker/go-common/autowire" - "reflect" ) type IAPIModule interface { diff --git a/module/ai-key/dto/enum.go b/module/ai-key/dto/enum.go new file mode 100644 index 00000000..29a26280 --- /dev/null +++ b/module/ai-key/dto/enum.go @@ -0,0 +1,49 @@ +package ai_key_dto + +var ( + KeyNormal KeyStatus = "normal" + KeyExceed KeyStatus = "exceeded" + KeyExpired KeyStatus = "expired" + KeyDisable KeyStatus = "disabled" + KeyError KeyStatus = "error" +) + +type KeyStatus string + +func (s KeyStatus) String() string { + return string(s) +} + +func (s KeyStatus) Int() int { + switch s { + case KeyDisable: + return 0 + case KeyNormal: + return 1 + case KeyError: + return 2 + case KeyExceed: + return 3 + case KeyExpired: + return 4 + default: + return 0 + } +} + +func ToKeyStatus(status int) KeyStatus { + switch status { + case 0: + return KeyDisable + case 1: + return KeyNormal + case 2: + return KeyError + case 3: + return KeyExceed + case 4: + return KeyExpired + default: + return KeyDisable + } +} diff --git a/module/ai-key/dto/input.go b/module/ai-key/dto/input.go new file mode 100644 index 00000000..2559a4ee --- /dev/null +++ b/module/ai-key/dto/input.go @@ -0,0 +1,20 @@ +package ai_key_dto + +type Create struct { + Id string `json:"id"` + Name string `json:"name"` + Config string `json:"config"` + ExpireTime int `json:"expire_time"` +} + +type Edit struct { + Name *string `json:"name"` + Config *string `json:"config"` + ExpireTime *int `json:"expire_time"` +} + +type Sort struct { + Origin string `json:"origin"` + Target string `json:"target"` + Sort string `json:"sort"` +} diff --git a/module/ai-key/dto/output.go b/module/ai-key/dto/output.go new file mode 100644 index 00000000..4e1da2c9 --- /dev/null +++ b/module/ai-key/dto/output.go @@ -0,0 +1,21 @@ +package ai_key_dto + +import "github.com/eolinker/go-common/auto" + +type Item struct { + Id string `json:"id"` + Name string `json:"name"` + Status KeyStatus `json:"status"` + UseToken int `json:"use_token"` + UpdateTime auto.TimeLabel `json:"update_time"` + ExpireTime int `json:"expire_time"` + Priority int `json:"priority"` + CanDelete bool `json:"can_delete"` +} + +type Key struct { + Id string `json:"id"` + Name string `json:"name"` + Config string `json:"config"` + ExpireTime int `json:"expire_time"` +} diff --git a/module/ai-key/iml.go b/module/ai-key/iml.go new file mode 100644 index 00000000..21cf216e --- /dev/null +++ b/module/ai-key/iml.go @@ -0,0 +1,447 @@ +package ai_key + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/APIParkLab/APIPark/service/cluster" + "github.com/eolinker/eosc/log" + + "github.com/APIParkLab/APIPark/gateway" + + "github.com/eolinker/go-common/utils" + + "gorm.io/gorm" + + "github.com/eolinker/go-common/auto" + + model_runtime "github.com/APIParkLab/APIPark/ai-provider/model-runtime" + + "github.com/google/uuid" + + "github.com/eolinker/go-common/store" + + "github.com/APIParkLab/APIPark/service/ai" + + ai_key_dto "github.com/APIParkLab/APIPark/module/ai-key/dto" + ai_key "github.com/APIParkLab/APIPark/service/ai-key" +) + +var _ IKeyModule = &imlKeyModule{} + +type imlKeyModule struct { + providerService ai.IProviderService `autowired:""` + aiKeyService ai_key.IKeyService `autowired:""` + clusterService cluster.IClusterService `autowired:""` + transaction store.ITransaction `autowired:""` +} + +func newKey(key *ai_key.Key) *gateway.DynamicRelease { + + return &gateway.DynamicRelease{ + BasicItem: &gateway.BasicItem{ + ID: fmt.Sprintf("%s-%s", key.Provider, key.ID), + Description: key.Name, + Resource: "ai-key", + Version: time.Now().Format("20060102150405"), + MatchLabels: map[string]string{ + "module": "ai-key", + }, + }, + Attr: map[string]interface{}{ + "expired": key.ExpireTime, + "config": key.Config, + "provider": key.Provider, + "priority": key.Priority, + "disabled": key.Status == 0, + }, + } +} + +func (i *imlKeyModule) Create(ctx context.Context, providerId string, input *ai_key_dto.Create) error { + _, err := i.providerService.Get(ctx, providerId) + if err != nil { + return fmt.Errorf("provider not found: %w", err) + } + p, has := model_runtime.GetProvider(providerId) + if !has { + return fmt.Errorf("provider not found: %w", err) + } + p.URI() + err = p.Check(input.Config) + if err != nil { + return fmt.Errorf("config check failed: %w", err) + } + priority, err := i.aiKeyService.MaxPriority(ctx, providerId) + if err != nil { + return fmt.Errorf("get key error: %v", err) + } + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + if input.Id == "" { + input.Id = uuid.NewString() + } + status := ai_key_dto.KeyNormal.Int() + if input.ExpireTime > 0 && time.Unix(int64(input.ExpireTime), 0).Before(time.Now()) { + status = ai_key_dto.KeyExpired.Int() + } + + err = i.aiKeyService.Create(ctx, &ai_key.Create{ + ID: input.Id, + Name: input.Name, + Config: input.Config, + Provider: providerId, + Status: status, + ExpireTime: input.ExpireTime, + Priority: priority + 1, + }) + + info, _ := i.aiKeyService.Get(ctx, input.Id) + releases := []*gateway.DynamicRelease{newKey(info)} + return i.syncGateway(ctx, cluster.DefaultClusterID, releases, true) + }) +} + +func (i *imlKeyModule) syncGateway(ctx context.Context, clusterId string, releases []*gateway.DynamicRelease, online bool) error { + client, err := i.clusterService.GatewayClient(ctx, clusterId) + if err != nil { + log.Errorf("get apinto client error: %v", err) + return nil + } + defer func() { + err := client.Close(ctx) + if err != nil { + log.Warn("close apinto client:", err) + } + }() + for _, releaseInfo := range releases { + dynamicClient, err := client.Dynamic(releaseInfo.Resource) + if err != nil { + return err + } + if online { + err = dynamicClient.Online(ctx, releaseInfo) + } else { + err = dynamicClient.Offline(ctx, releaseInfo) + } + if err != nil { + return err + } + } + + return nil +} + +func (i *imlKeyModule) Edit(ctx context.Context, providerId string, id string, input *ai_key_dto.Edit) error { + p, has := model_runtime.GetProvider(providerId) + if !has { + return fmt.Errorf("provider not found: %s", providerId) + } + _, err := i.providerService.Get(ctx, providerId) + if err != nil { + return fmt.Errorf("provider not found: %w", err) + } + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + info, err := i.aiKeyService.Get(ctx, id) + if err != nil { + return fmt.Errorf("key not found: %w", err) + } + if input.Config != nil { + err = p.Check(*input.Config) + if err != nil { + return fmt.Errorf("config check failed: %w", err) + } + cfg, err := p.GenConfig(*input.Config, info.Config) + if err != nil { + return fmt.Errorf("config gen failed: %w", err) + } + input.Config = &cfg + if info.Default { + err = i.providerService.Save(ctx, info.Provider, &ai.SetProvider{ + Config: input.Config, + }) + if err != nil { + return err + } + } + } + + status := ai_key_dto.KeyNormal.Int() + orgStatus := ai_key_dto.ToKeyStatus(info.Status) + switch orgStatus { + case ai_key_dto.KeyNormal, ai_key_dto.KeyError, ai_key_dto.KeyExpired: + if input.ExpireTime != nil { + expireTime := *input.ExpireTime + if expireTime > 0 && time.Unix(int64(expireTime), 0).Before(time.Now()) { + status = ai_key_dto.KeyExpired.Int() + } + } else if info.ExpireTime > 0 && time.Unix(int64(info.ExpireTime), 0).Before(time.Now()) { + // 如果过期时间未更改,且已过期,则设置为过期状态 + status = ai_key_dto.KeyExpired.Int() + } + default: + // 停用、超额需要启用,所以维持原状态 + status = orgStatus.Int() + } + + err = i.aiKeyService.Save(ctx, id, &ai_key.Edit{ + Name: input.Name, + Config: input.Config, + ExpireTime: input.ExpireTime, + Status: &status, + }) + if err != nil { + return err + } + if status == ai_key_dto.KeyNormal.Int() { + info, err = i.aiKeyService.Get(ctx, id) + if err != nil { + return err + } + releases := []*gateway.DynamicRelease{newKey(info)} + return i.syncGateway(ctx, cluster.DefaultClusterID, releases, true) + } + return nil + }) + +} + +func (i *imlKeyModule) Delete(ctx context.Context, providerId string, id string) error { + _, err := i.providerService.Get(ctx, providerId) + if err != nil { + return fmt.Errorf("provider not found: %w", err) + } + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + info, err := i.aiKeyService.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + if info.Default { + return fmt.Errorf("default key can't be deleted: %s", id) + } + keys, err := i.aiKeyService.KeysAfterPriority(ctx, providerId, info.Priority) + if err != nil { + return err + } + for _, key := range keys { + key.Priority-- + err = i.aiKeyService.Save(ctx, key.ID, &ai_key.Edit{ + Priority: &key.Priority, + }) + if err != nil { + return err + } + } + + err = i.aiKeyService.Delete(ctx, id) + if err != nil { + return err + } + return i.syncGateway(ctx, cluster.DefaultClusterID, []*gateway.DynamicRelease{{ + BasicItem: &gateway.BasicItem{ + ID: fmt.Sprintf("%s-%s", providerId, id), + Resource: "ai-key", + }, + Attr: nil, + }, + }, false) + }) +} + +func (i *imlKeyModule) Get(ctx context.Context, providerId string, id string) (*ai_key_dto.Key, error) { + p, has := model_runtime.GetProvider(providerId) + if !has { + return nil, fmt.Errorf("provider not found: %s", providerId) + } + _, err := i.providerService.Get(ctx, providerId) + if err != nil { + return nil, fmt.Errorf("provider not found: %w", err) + } + info, err := i.aiKeyService.Get(ctx, id) + if err != nil { + return nil, fmt.Errorf("key not found: %w", err) + } + + return &ai_key_dto.Key{ + Id: info.ID, + Name: info.Name, + Config: p.MaskConfig(info.Config), + ExpireTime: info.ExpireTime, + }, nil +} + +func (i *imlKeyModule) List(ctx context.Context, providerId string, keyword string, page, pageSize int, statuses []string) ([]*ai_key_dto.Item, int64, error) { + _, err := i.aiKeyService.DefaultKey(ctx, providerId) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, 0, fmt.Errorf("get default key failed: %w", err) + } + info, err := i.providerService.Get(ctx, providerId) + if err != nil { + return nil, 0, fmt.Errorf("provider is unconfigued,id is %s", providerId) + } + err = i.aiKeyService.Create(ctx, &ai_key.Create{ + ID: info.Id, + Name: info.Name, + Config: info.Config, + Provider: info.Id, + Status: ai_key_dto.KeyNormal.Int(), + Priority: 1, + ExpireTime: 0, + UseToken: 0, + Default: true, + }) + if err != nil { + return nil, 0, fmt.Errorf("create default key failed: %w", err) + } + } + w := map[string]interface{}{ + "provider": providerId, + } + hasExpired := true + if len(statuses) > 0 { + hasExpired = false + statusConditions := make([]int, 0, len(statuses)) + for _, s := range statuses { + status := ai_key_dto.KeyStatus(s) + if status == ai_key_dto.KeyExpired { + hasExpired = true + } + statusConditions = append(statusConditions, status.Int()) + } + w["status"] = statusConditions + } + var list []*ai_key.Key + var total int64 + if !hasExpired { + if keyword != "" { + list, err = i.aiKeyService.Search(ctx, keyword, w, "sort asc") + if err != nil { + return nil, 0, err + } + if len(list) == 0 { + return nil, 0, nil + } + uuids := utils.SliceToSlice(list, func(key *ai_key.Key) string { + return key.ID + }) + w["uuid"] = uuids + } + list, total, err = i.aiKeyService.SearchUnExpiredByPage(ctx, w, page, pageSize, "sort asc") + if err != nil { + return nil, 0, err + } + } else { + list, total, err = i.aiKeyService.SearchByPage(ctx, keyword, w, page, pageSize, "sort asc") + if err != nil { + return nil, 0, err + } + } + + var result []*ai_key_dto.Item + for _, item := range list { + status := ai_key_dto.ToKeyStatus(item.Status) + if item.ExpireTime > 0 && time.Unix(int64(item.ExpireTime), 0).Before(time.Now()) { + status = ai_key_dto.KeyExpired + } + result = append(result, &ai_key_dto.Item{ + Id: item.ID, + Name: item.Name, + Status: status, + UseToken: item.UseToken, + UpdateTime: auto.TimeLabel(item.UpdateAt), + ExpireTime: item.ExpireTime, + CanDelete: !item.Default, + Priority: item.Priority, + }) + } + + return result, total, nil +} + +func (i *imlKeyModule) UpdateKeyStatus(ctx context.Context, providerId string, id string, enable bool) error { + _, err := i.providerService.Get(ctx, providerId) + if err != nil { + return fmt.Errorf("provider not found: %w", err) + } + info, err := i.aiKeyService.Get(ctx, id) + if err != nil { + return fmt.Errorf("key not found: %w", err) + } + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + if !enable { + status := ai_key_dto.KeyDisable.Int() + err = i.aiKeyService.Save(ctx, id, &ai_key.Edit{ + Status: &status, + }) + if err != nil { + return err + } + releases := []*gateway.DynamicRelease{{ + BasicItem: &gateway.BasicItem{ + ID: id, + Resource: "ai-key", + }, + Attr: nil, + }} + return i.syncGateway(ctx, providerId, releases, false) + } + if info.Status == ai_key_dto.KeyDisable.Int() || info.Status == ai_key_dto.KeyExceed.Int() { + // 超额 或 停用状态,可启用 + if info.ExpireTime > 0 && time.Unix(int64(info.ExpireTime), 0).Before(time.Now()) { + // 如果过期时间未更改,且已过期,则设置为过期状态 + status := ai_key_dto.KeyExpired.Int() + return i.aiKeyService.Save(ctx, id, &ai_key.Edit{ + Status: &status, + }) + } + status := ai_key_dto.KeyNormal.Int() + err = i.aiKeyService.Save(ctx, id, &ai_key.Edit{ + Status: &status, + }) + if err != nil { + return err + } + info, err = i.aiKeyService.Get(ctx, id) + if err != nil { + return err + } + releases := []*gateway.DynamicRelease{newKey(info)} + return i.syncGateway(ctx, providerId, releases, true) + } + return nil + }) +} + +func (i *imlKeyModule) Sort(ctx context.Context, providerId string, input *ai_key_dto.Sort) error { + _, err := i.providerService.Get(ctx, providerId) + if err != nil { + return fmt.Errorf("provider not found: %w", err) + } + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + switch input.Sort { + case "before": + _, err = i.aiKeyService.SortBefore(ctx, providerId, input.Origin, input.Target) + case "after": + _, err = i.aiKeyService.SortAfter(ctx, providerId, input.Origin, input.Target) + default: + return fmt.Errorf("invalid sort type: %s", input.Sort) + } + if err != nil { + return err + } + list, err := i.aiKeyService.KeysByProvider(ctx, providerId) + if err != nil { + return err + } + releases := make([]*gateway.DynamicRelease, 0, len(list)) + for _, info := range list { + releases = append(releases, newKey(info)) + } + return i.syncGateway(ctx, cluster.DefaultClusterID, releases, true) + }) +} diff --git a/module/ai-key/module.go b/module/ai-key/module.go new file mode 100644 index 00000000..30ddebbb --- /dev/null +++ b/module/ai-key/module.go @@ -0,0 +1,25 @@ +package ai_key + +import ( + "context" + "reflect" + + ai_key_dto "github.com/APIParkLab/APIPark/module/ai-key/dto" + "github.com/eolinker/go-common/autowire" +) + +type IKeyModule interface { + Create(ctx context.Context, providerId string, input *ai_key_dto.Create) error + Edit(ctx context.Context, providerId string, id string, input *ai_key_dto.Edit) error + Delete(ctx context.Context, providerId string, id string) error + Get(ctx context.Context, providerId string, id string) (*ai_key_dto.Key, error) + List(ctx context.Context, providerId string, keyword string, page, pageSize int, statuses []string) ([]*ai_key_dto.Item, int64, error) + UpdateKeyStatus(ctx context.Context, providerId string, id string, enable bool) error + Sort(ctx context.Context, providerId string, input *ai_key_dto.Sort) error +} + +func init() { + autowire.Auto[IKeyModule](func() reflect.Value { + return reflect.ValueOf(new(imlKeyModule)) + }) +} diff --git a/module/ai/dto/enum.go b/module/ai/dto/enum.go new file mode 100644 index 00000000..2cc76b6f --- /dev/null +++ b/module/ai/dto/enum.go @@ -0,0 +1,39 @@ +package ai_dto + +var ( + ProviderEnabled ProviderStatus = "enabled" + ProviderDisabled ProviderStatus = "disabled" + ProviderAbnormal ProviderStatus = "abnormal" +) + +type ProviderStatus string + +func (p ProviderStatus) Int() int { + switch p { + case ProviderAbnormal: + return 2 + case ProviderEnabled: + return 1 + case ProviderDisabled: + return 0 + default: + return 1 + } +} + +func (p ProviderStatus) String() string { + return string(p) +} + +func ToProviderStatus(status int) ProviderStatus { + switch status { + case 2: + return ProviderAbnormal + case 0: + return ProviderDisabled + case 1: + return ProviderEnabled + default: + return ProviderEnabled + } +} diff --git a/module/ai/dto/input.go b/module/ai/dto/input.go index 8f1b4c48..93a112f8 100644 --- a/module/ai/dto/input.go +++ b/module/ai/dto/input.go @@ -7,4 +7,10 @@ type UpdateLLM struct { type UpdateConfig struct { DefaultLLM string `json:"default_llm"` Config string `json:"config"` + Priority *int `json:"priority"` + Enable *bool `json:"enable"` +} + +type Sort struct { + Providers []string `json:"providers"` } diff --git a/module/ai/dto/output.go b/module/ai/dto/output.go index c21f7ceb..83914ae6 100644 --- a/module/ai/dto/output.go +++ b/module/ai/dto/output.go @@ -1,33 +1,71 @@ package ai_dto -import "time" +import ( + "github.com/eolinker/go-common/auto" +) + +type SimpleProvider struct { + Id string `json:"id"` + Name string `json:"name"` + DefaultConfig string `json:"default_config"` + Logo string `json:"logo"` + GetAPIKeyUrl string `json:"get_apikey_url"` +} type Provider struct { - Id string `json:"id"` - Name string `json:"name"` - Config string `json:"config"` - GetAPIKeyUrl string `json:"get_apikey_url"` - DefaultLLM string `json:"defaultLLM"` - DefaultLLMConfig string `json:"-"` + Id string `json:"id"` + Name string `json:"name"` + Config string `json:"config"` + GetAPIKeyUrl string `json:"get_apikey_url"` + DefaultLLM string `json:"default_llm"` + DefaultLLMConfig string `json:"-"` + Priority int `json:"priority"` + Status ProviderStatus `json:"status"` + Configured bool `json:"configured"` +} + +type ConfiguredProviderItem struct { + Id string `json:"id"` + Name string `json:"name"` + Logo string `json:"logo"` + DefaultLLM string `json:"default_llm"` + Status ProviderStatus `json:"status"` + APICount int64 `json:"api_count"` + KeyCount int `json:"key_count"` + KeyStatus []*KeyStatus `json:"keys"` + Priority int `json:"priority"` +} + +type KeyStatus struct { + Id string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Priority int `json:"-"` } type ProviderItem struct { - Id string `json:"id"` - Name string `json:"name"` - DefaultLLM string `json:"default_llm"` - DefaultLLMLogo string `json:"default_llm_logo"` - Logo string `json:"logo"` - Configured bool `json:"configured"` - Recommend bool `json:"recommend"` - Sort int `json:"sort"` - UpdateTime time.Time `json:"-"` -} - -type SimpleProviderItem struct { Id string `json:"id"` Name string `json:"name"` Logo string `json:"logo"` - Configured bool `json:"configured"` + DefaultLLM string `json:"default_llm"` + Sort int `json:"-"` +} + +type SimpleProviderItem struct { + Id string `json:"id"` + Name string `json:"name"` + Logo string `json:"logo"` + Configured bool `json:"configured"` + DefaultConfig string `json:"default_config"` + Status ProviderStatus `json:"status"` + Model *BasicInfo `json:"model,omitempty"` + Priority int `json:"-"` +} + +type BackupProvider struct { + Id string `json:"id"` + Name string `json:"name"` + Model *BasicInfo `json:"model,omitempty"` } type LLMItem struct { @@ -36,3 +74,26 @@ type LLMItem struct { Config string `json:"config"` Scopes []string `json:"scopes"` } + +type APIItem struct { + Id string `json:"id"` + Name string `json:"name"` + Service auto.Label `json:"service" aolabel:"service"` + Team auto.Label `json:"team" aolabel:"team"` + Method string `json:"method"` + RequestPath string `json:"request_path"` + Model auto.Label `json:"model"` + UpdateTime auto.TimeLabel `json:"update_time"` + UseToken int `json:"use_token"` + Disable bool `json:"disable"` +} + +type Condition struct { + Models []*BasicInfo `json:"models"` + Services []*BasicInfo `json:"services"` +} + +type BasicInfo struct { + Id string `json:"id"` + Name string `json:"name"` +} diff --git a/module/ai/iml.go b/module/ai/iml.go index f830a8ac..201ff398 100644 --- a/module/ai/iml.go +++ b/module/ai/iml.go @@ -2,12 +2,22 @@ package ai import ( "context" - "encoding/json" "errors" "fmt" + "net/http" "sort" "time" + "github.com/APIParkLab/APIPark/service/service" + + ai_key_dto "github.com/APIParkLab/APIPark/module/ai-key/dto" + + ai_key "github.com/APIParkLab/APIPark/service/ai-key" + + "github.com/eolinker/go-common/auto" + + ai_api "github.com/APIParkLab/APIPark/service/ai-api" + model_runtime "github.com/APIParkLab/APIPark/ai-provider/model-runtime" "github.com/APIParkLab/APIPark/gateway" ai_dto "github.com/APIParkLab/APIPark/module/ai/dto" @@ -19,24 +29,24 @@ import ( "gorm.io/gorm" ) -func newAIUpstream(provider string, uri model_runtime.IProviderURI) *gateway.DynamicRelease { +func newKey(key *ai_key.Key) *gateway.DynamicRelease { + return &gateway.DynamicRelease{ BasicItem: &gateway.BasicItem{ - ID: provider, - Description: fmt.Sprintf("auto create by ai provider %s", provider), - Resource: "service", + ID: fmt.Sprintf("%s-%s", key.Provider, key.ID), + Description: key.Name, + Resource: "ai-key", Version: time.Now().Format("20060102150405"), MatchLabels: map[string]string{ - "module": "service", + "module": "ai-key", }, }, Attr: map[string]interface{}{ - "driver": "http", - "balance": "round-robin", - "nodes": []string{fmt.Sprintf("%s weight=100", uri.Host())}, - "pass_host": "node", - "scheme": uri.Scheme(), - "timeout": 300000, + "expired": key.ExpireTime, + "config": key.Config, + "provider": key.Provider, + "priority": key.Priority, + "disabled": key.Status == 0, }, } } @@ -46,9 +56,192 @@ var _ IProviderModule = (*imlProviderModule)(nil) type imlProviderModule struct { providerService ai.IProviderService `autowired:""` clusterService cluster.IClusterService `autowired:""` + aiAPIService ai_api.IAPIService `autowired:""` + aiKeyService ai_key.IKeyService `autowired:""` transaction store.ITransaction `autowired:""` } +func (i *imlProviderModule) SimpleProvider(ctx context.Context, id string) (*ai_dto.SimpleProvider, error) { + p, has := model_runtime.GetProvider(id) + if !has { + return nil, fmt.Errorf("ai provider not found") + } + return &ai_dto.SimpleProvider{ + Id: p.ID(), + Name: p.Name(), + Logo: p.Logo(), + DefaultConfig: p.DefaultConfig(), + GetAPIKeyUrl: p.HelpUrl(), + }, nil +} + +func (i *imlProviderModule) Sort(ctx context.Context, input *ai_dto.Sort) error { + return i.transaction.Transaction(ctx, func(txCtx context.Context) error { + list, err := i.providerService.List(ctx) + if err != nil { + return err + } + providerMap := utils.SliceToMap(list, func(e *ai.Provider) string { + return e.Id + }) + releases := make([]*gateway.DynamicRelease, 0, len(list)) + offlineReleases := make([]*gateway.DynamicRelease, 0, len(list)) + for index, id := range input.Providers { + p, has := model_runtime.GetProvider(id) + if !has { + continue + } + + l, has := providerMap[id] + if !has { + continue + } + model, has := p.GetModel(l.DefaultLLM) + if !has { + continue + } + priority := index + 1 + err = i.providerService.Save(txCtx, id, &ai.SetProvider{ + Priority: &priority, + }) + if err != nil { + return err + } + if ai_dto.ToProviderStatus(l.Status) == ai_dto.ProviderDisabled { + offlineReleases = append(offlineReleases, &gateway.DynamicRelease{ + BasicItem: &gateway.BasicItem{ + ID: l.Id, + Resource: "ai-provider", + }}) + } else { + cfg := make(map[string]interface{}) + cfg["provider"] = l.Id + cfg["model"] = l.DefaultLLM + cfg["model_config"] = model.DefaultConfig() + cfg["priority"] = l.Priority + cfg["base"] = fmt.Sprintf("%s://%s", p.URI().Scheme(), p.URI().Host()) + releases = append(releases, &gateway.DynamicRelease{ + BasicItem: &gateway.BasicItem{ + ID: l.Id, + Description: l.Name, + Resource: "ai-provider", + Version: l.UpdateAt.Format("20060102150405"), + MatchLabels: map[string]string{ + "module": "ai-provider", + }, + }, + Attr: cfg, + }) + } + } + err = i.syncGateway(ctx, cluster.DefaultClusterID, releases, true) + if err != nil { + return err + } + return i.syncGateway(ctx, cluster.DefaultClusterID, offlineReleases, false) + + }) +} + +func (i *imlProviderModule) ConfiguredProviders(ctx context.Context) ([]*ai_dto.ConfiguredProviderItem, *ai_dto.BackupProvider, error) { + // 获取已配置的AI服务商 + list, err := i.providerService.List(ctx) + if err != nil { + return nil, nil, fmt.Errorf("get provider list error:%v", err) + } + aiAPIMap, err := i.aiAPIService.CountMapByProvider(ctx, "", nil) + if err != nil { + return nil, nil, fmt.Errorf("get ai api count error:%v", err) + } + providers := make([]*ai_dto.ConfiguredProviderItem, 0, len(list)) + for _, l := range list { + // 检查是否有默认Key + _, err = i.aiKeyService.DefaultKey(ctx, l.Id) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil, err + } + err = i.aiKeyService.Create(ctx, &ai_key.Create{ + ID: l.Id, + Name: l.Name, + Config: l.Config, + Provider: l.Id, + Priority: 1, + Status: 1, + ExpireTime: 0, + UseToken: 0, + Default: true, + }) + if err != nil { + return nil, nil, fmt.Errorf("create default key error:%v", err) + } + } + + p, has := model_runtime.GetProvider(l.Id) + if !has { + continue + } + keys, err := i.aiKeyService.KeysByProvider(ctx, l.Id) + if err != nil { + return nil, nil, fmt.Errorf("get provider keys error:%v", err) + } + + keysStatus := make([]*ai_dto.KeyStatus, 0, len(keys)) + for _, k := range keys { + status := ai_key_dto.ToKeyStatus(k.Status) + switch status { + case ai_key_dto.KeyNormal, ai_key_dto.KeyDisable, ai_key_dto.KeyError: + default: + status = ai_key_dto.KeyError + } + keysStatus = append(keysStatus, &ai_dto.KeyStatus{ + Id: k.ID, + Name: k.Name, + Status: status.String(), + Priority: k.Priority, + }) + } + sort.Slice(keysStatus, func(i, j int) bool { + return keysStatus[i].Priority < keysStatus[j].Priority + }) + + providers = append(providers, &ai_dto.ConfiguredProviderItem{ + Id: l.Id, + Name: l.Name, + Logo: p.Logo(), + DefaultLLM: l.DefaultLLM, + Status: ai_dto.ToProviderStatus(l.Status), + APICount: aiAPIMap[l.Id], + KeyCount: len(keysStatus), + KeyStatus: keysStatus, + Priority: l.Priority, + }) + } + sort.Slice(providers, func(i, j int) bool { + if providers[i].Priority != providers[j].Priority { + if providers[i].Priority == 0 { + return false + } + if providers[j].Priority == 0 { + return true + } + return providers[i].Priority < providers[j].Priority + } + return providers[i].Name < providers[j].Name + }) + var backup *ai_dto.BackupProvider + for _, p := range providers { + if p.Status == ai_dto.ProviderEnabled { + backup = &ai_dto.BackupProvider{ + Id: p.Id, + Name: p.Name, + } + break + } + } + return providers, backup, nil +} + func (i *imlProviderModule) SimpleProviders(ctx context.Context) ([]*ai_dto.SimpleProviderItem, error) { list, err := i.providerService.List(ctx) if err != nil { @@ -62,19 +255,95 @@ func (i *imlProviderModule) SimpleProviders(ctx context.Context) ([]*ai_dto.Simp items := make([]*ai_dto.SimpleProviderItem, 0, len(providers)) for _, v := range providers { item := &ai_dto.SimpleProviderItem{ - Id: v.ID(), - Name: v.Name(), - Logo: v.Logo(), + Id: v.ID(), + Name: v.Name(), + Logo: v.Logo(), + DefaultConfig: v.DefaultConfig(), + Status: ai_dto.ProviderDisabled, } - if _, has := providerMap[v.ID()]; has { + if info, has := providerMap[v.ID()]; has { item.Configured = true + item.Status = ai_dto.ToProviderStatus(info.Status) + item.Priority = info.Priority } items = append(items, item) } + sort.Slice(items, func(i, j int) bool { + if items[i].Priority != items[j].Priority { + if items[i].Priority == 0 { + return false + } + if items[j].Priority == 0 { + return true + } + return items[i].Priority < items[j].Priority + } + return items[i].Name < items[j].Name + }) return items, nil } -func (i *imlProviderModule) Providers(ctx context.Context) ([]*ai_dto.ProviderItem, error) { +func (i *imlProviderModule) SimpleConfiguredProviders(ctx context.Context) ([]*ai_dto.SimpleProviderItem, *ai_dto.BackupProvider, error) { + list, err := i.providerService.List(ctx) + if err != nil { + return nil, nil, err + } + items := make([]*ai_dto.SimpleProviderItem, 0, len(list)) + var backup *ai_dto.BackupProvider + for _, l := range list { + p, has := model_runtime.GetProvider(l.Id) + if !has { + continue + } + model, has := p.GetModel(l.DefaultLLM) + if !has { + model, has = p.DefaultModel(model_runtime.ModelTypeLLM) + if !has { + continue + } + } + item := &ai_dto.SimpleProviderItem{ + Id: l.Id, + Name: l.Name, + Logo: p.Logo(), + DefaultConfig: p.DefaultConfig(), + Status: ai_dto.ToProviderStatus(l.Status), + Priority: l.Priority, + Configured: true, + Model: &ai_dto.BasicInfo{ + Id: model.ID(), + Name: model.ID(), + }, + } + + items = append(items, item) + } + sort.Slice(items, func(i, j int) bool { + if items[i].Priority != items[j].Priority { + if items[i].Priority == 0 { + return false + } + if items[j].Priority == 0 { + return true + } + return items[i].Priority < items[j].Priority + } + return items[i].Name < items[j].Name + }) + for _, item := range items { + if item.Status == ai_dto.ProviderEnabled { + backup = &ai_dto.BackupProvider{ + Id: item.Id, + Name: item.Name, + Model: item.Model, + } + break + } + } + return items, backup, nil +} + +func (i *imlProviderModule) UnConfiguredProviders(ctx context.Context) ([]*ai_dto.ProviderItem, error) { list, err := i.providerService.List(ctx) if err != nil { return nil, err @@ -85,30 +354,21 @@ func (i *imlProviderModule) Providers(ctx context.Context) ([]*ai_dto.ProviderIt }) items := make([]*ai_dto.ProviderItem, 0, len(providers)) for _, v := range providers { - - item := &ai_dto.ProviderItem{ - Id: v.ID(), - Name: v.Name(), - Logo: v.Logo(), - Recommend: v.Recommend(), - Sort: v.Sort(), + if _, has := providerMap[v.ID()]; has { + // 已配置,跳过 + continue } - if info, has := providerMap[v.ID()]; has { - defaultLLM, has := v.GetModel(info.DefaultLLM) - if !has { - continue - } - item.Configured = true - item.DefaultLLM = defaultLLM.ID() - item.DefaultLLMLogo = defaultLLM.Logo() - item.UpdateTime = info.UpdateAt + defaultLLM, _ := v.DefaultModel(model_runtime.ModelTypeLLM) + item := &ai_dto.ProviderItem{ + Id: v.ID(), + Name: v.Name(), + Logo: v.Logo(), + DefaultLLM: defaultLLM.ID(), + Sort: v.Sort(), } items = append(items, item) } sort.Slice(items, func(i, j int) bool { - if items[i].Configured == items[j].Configured && items[i].Configured { - return items[i].Name < items[j].Name - } if items[i].Sort != items[j].Sort { if items[i].Sort == 0 { return false @@ -128,6 +388,13 @@ func (i *imlProviderModule) Provider(ctx context.Context, id string) (*ai_dto.Pr if !has { return nil, fmt.Errorf("ai provider not found") } + maxPriority, err := i.providerService.MaxPriority(ctx) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + } + maxPriority = maxPriority + 1 info, err := i.providerService.Get(ctx, id) if err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { @@ -144,6 +411,8 @@ func (i *imlProviderModule) Provider(ctx context.Context, id string) (*ai_dto.Pr GetAPIKeyUrl: p.HelpUrl(), DefaultLLM: defaultLLM.ID(), DefaultLLMConfig: defaultLLM.Logo(), + Status: ai_dto.ProviderDisabled, + Priority: maxPriority, }, nil } defaultLLM, has := p.GetModel(info.DefaultLLM) @@ -154,6 +423,10 @@ func (i *imlProviderModule) Provider(ctx context.Context, id string) (*ai_dto.Pr } defaultLLM = model } + if info.Priority == 0 { + info.Priority = maxPriority + } + return &ai_dto.Provider{ Id: info.Id, Name: info.Name, @@ -161,6 +434,9 @@ func (i *imlProviderModule) Provider(ctx context.Context, id string) (*ai_dto.Pr GetAPIKeyUrl: p.HelpUrl(), DefaultLLM: defaultLLM.ID(), DefaultLLMConfig: defaultLLM.DefaultConfig(), + Priority: info.Priority, + Status: ai_dto.ToProviderStatus(info.Status), + Configured: true, }, nil } @@ -196,78 +472,19 @@ func (i *imlProviderModule) LLMs(ctx context.Context, driver string) ([]*ai_dto. return nil, nil, fmt.Errorf("ai provider default llm not found") } return items, &ai_dto.ProviderItem{ - Id: p.ID(), - Name: p.Name(), - DefaultLLM: defaultLLM.ID(), - DefaultLLMLogo: defaultLLM.Logo(), - Logo: p.Logo(), - Configured: false, + Id: p.ID(), + Name: p.Name(), + DefaultLLM: defaultLLM.ID(), + Logo: p.Logo(), }, nil } - return items, &ai_dto.ProviderItem{Id: info.Id, Name: info.Name, DefaultLLM: info.DefaultLLM, Logo: p.Logo(), Configured: true}, nil -} - -func (i *imlProviderModule) UpdateProviderStatus(ctx context.Context, id string, enable bool) error { - driver, has := model_runtime.GetProvider(id) - if !has { - return fmt.Errorf("ai provider not found") - } - info, err := i.providerService.Get(ctx, id) - if err != nil { - if !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - return fmt.Errorf("ai provider not found") - } - - return i.transaction.Transaction(ctx, func(txCtx context.Context) error { - err = i.providerService.Save(txCtx, id, &ai.SetProvider{ - Status: &enable, - }) - if err != nil { - return err - } - if enable { - cfg := make(map[string]interface{}) - err = json.Unmarshal([]byte(info.Config), &cfg) - if err != nil { - log.Errorf("unmarshal ai provider config error,id is %s,err is %v", info.Id, err) - return err - } - cfg["driver"] = info.Id - - return i.syncGateway(txCtx, cluster.DefaultClusterID, []*gateway.DynamicRelease{{ - BasicItem: &gateway.BasicItem{ - ID: info.Id, - Description: info.Name, - Version: info.UpdateAt.Format("20060102150405"), - MatchLabels: map[string]string{ - "module": "ai-provider", - }, - }, - Attr: cfg, - }, newAIUpstream(info.Id, driver.URI()), - }, enable) - } else { - return i.syncGateway(txCtx, cluster.DefaultClusterID, []*gateway.DynamicRelease{ - { - BasicItem: &gateway.BasicItem{ - ID: info.Id, - Resource: info.Id, - }, - }, - { - BasicItem: &gateway.BasicItem{ - ID: info.Id, - Resource: "service", - }, - }, - }, enable) - } - - }) - + return items, &ai_dto.ProviderItem{ + Id: info.Id, + Name: info.Name, + DefaultLLM: info.DefaultLLM, + Logo: p.Logo(), + }, nil } func (i *imlProviderModule) UpdateProviderConfig(ctx context.Context, id string, input *ai_dto.UpdateConfig) error { @@ -294,6 +511,10 @@ func (i *imlProviderModule) UpdateProviderConfig(ctx context.Context, id string, Config: input.Config, } } + model, has := p.GetModel(input.DefaultLLM) + if !has { + return fmt.Errorf("ai provider model not found") + } err = p.Check(input.Config) if err != nil { return err @@ -303,70 +524,120 @@ func (i *imlProviderModule) UpdateProviderConfig(ctx context.Context, id string, return err } return i.transaction.Transaction(ctx, func(txCtx context.Context) error { - err = i.providerService.Save(ctx, id, &ai.SetProvider{ + status := 0 + if input.Enable != nil && *input.Enable { + status = 1 + } + pInfo := &ai.SetProvider{ Name: &info.Name, - DefaultLLM: &info.DefaultLLM, + DefaultLLM: &input.DefaultLLM, Config: &input.Config, - }) + Priority: input.Priority, + Status: &status, + } + _, err = i.aiKeyService.DefaultKey(ctx, id) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = i.aiKeyService.Create(ctx, &ai_key.Create{ + ID: id, + Name: info.Name, + Config: info.Config, + Provider: id, + Status: 1, + ExpireTime: 0, + Default: true, + Priority: 1, + }) + } else { + err = i.aiKeyService.Save(ctx, id, &ai_key.Edit{ + Config: &info.Config, + }) + } + if err != nil { + return err + } + + //if input.Enable != nil { + // status = 0 + // if *input.Enable { + // status = 1 + // } + // pInfo.Status = &status + //} + err = i.providerService.Save(ctx, id, pInfo) + if err != nil { + return err + } + + if *pInfo.Status == 0 { + return i.syncGateway(ctx, cluster.DefaultClusterID, []*gateway.DynamicRelease{ + { + BasicItem: &gateway.BasicItem{ + ID: id, + Resource: "ai-provider", + }, + }, + }, false) + } + // 获取当前供应商所有Key信息 + defaultKey, err := i.aiKeyService.DefaultKey(ctx, id) if err != nil { return err } cfg := make(map[string]interface{}) - err = json.Unmarshal([]byte(input.Config), &cfg) - if err != nil { - log.Errorf("unmarshal ai provider config error,id is %s,err is %v", id, err) - return err - } - + cfg["provider"] = info.Id + cfg["model"] = info.DefaultLLM + cfg["model_config"] = model.DefaultConfig() + cfg["priority"] = info.Priority + cfg["base"] = fmt.Sprintf("%s://%s", p.URI().Scheme(), p.URI().Host()) return i.syncGateway(ctx, cluster.DefaultClusterID, []*gateway.DynamicRelease{ { BasicItem: &gateway.BasicItem{ ID: id, Description: info.Name, - Resource: id, + Resource: "ai-provider", Version: info.UpdateAt.Format("20060102150405"), MatchLabels: map[string]string{ "module": "ai-provider", }, }, Attr: cfg, - }, newAIUpstream(id, p.URI()), + }, newKey(defaultKey), }, true) }) } -func (i *imlProviderModule) UpdateProviderDefaultLLM(ctx context.Context, id string, input *ai_dto.UpdateLLM) error { - _, err := i.providerService.Get(ctx, id) - if err != nil { - if !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - return fmt.Errorf("ai provider not found") - } - return i.providerService.Save(ctx, id, &ai.SetProvider{ - DefaultLLM: &input.LLM, - }) -} - func (i *imlProviderModule) getAiProviders(ctx context.Context) ([]*gateway.DynamicRelease, error) { list, err := i.providerService.List(ctx) if err != nil { return nil, err } + providers := make([]*gateway.DynamicRelease, 0, len(list)) - for _, p := range list { - cfg := make(map[string]interface{}) - err = json.Unmarshal([]byte(p.Config), &cfg) - if err != nil { - log.Errorf("unmarshal ai provider config error,id is %s,err is %v", p.Id, err) - continue + for _, l := range list { + // 获取当前供应商所有Key信息 + + driver, has := model_runtime.GetProvider(l.Id) + if !has { + return nil, fmt.Errorf("provider not found: %s", l.Id) } + model, has := driver.GetModel(l.DefaultLLM) + if !has { + return nil, fmt.Errorf("model not found: %s", l.DefaultLLM) + } + cfg := make(map[string]interface{}) + cfg["provider"] = l.Id + cfg["model"] = l.DefaultLLM + cfg["model_config"] = model.DefaultConfig() + cfg["priority"] = l.Priority providers = append(providers, &gateway.DynamicRelease{ BasicItem: &gateway.BasicItem{ - ID: p.Id, - Description: p.Name, - Resource: p.Id, - Version: p.UpdateAt.Format("20060102150405"), + ID: l.Id, + Description: l.Name, + Resource: "ai-provider", + Version: l.UpdateAt.Format("20060102150405"), MatchLabels: map[string]string{ "module": "ai-provider", }, @@ -376,21 +647,15 @@ func (i *imlProviderModule) getAiProviders(ctx context.Context) ([]*gateway.Dyna } return providers, nil } + func (i *imlProviderModule) initGateway(ctx context.Context, clusterId string, clientDriver gateway.IClientDriver) error { providers, err := i.getAiProviders(ctx) if err != nil { return err } - serviceClient, err := clientDriver.Dynamic("service") - if err != nil { - return err - } + for _, p := range providers { - driver, has := model_runtime.GetProvider(p.ID) - if !has { - continue - } - client, err := clientDriver.Dynamic(p.ID) + client, err := clientDriver.Dynamic(p.Resource) if err != nil { return err } @@ -398,12 +663,6 @@ func (i *imlProviderModule) initGateway(ctx context.Context, clusterId string, c if err != nil { return err } - - err = serviceClient.Online(ctx, newAIUpstream(p.ID, driver.URI())) - if err != nil { - return err - } - } return nil @@ -429,7 +688,7 @@ func (i *imlProviderModule) syncGateway(ctx context.Context, clusterId string, r if online { err = dynamicClient.Online(ctx, releaseInfo) } else { - err = dynamicClient.Offline(ctx, releaseInfo) + dynamicClient.Offline(ctx, releaseInfo) } if err != nil { return err @@ -438,3 +697,125 @@ func (i *imlProviderModule) syncGateway(ctx context.Context, clusterId string, r return nil } + +var _ IAIAPIModule = (*imlAIApiModule)(nil) + +type imlAIApiModule struct { + aiAPIService ai_api.IAPIService `autowired:""` + aiAPIUseService ai_api.IAPIUseService `autowired:""` + serviceService service.IServiceService `autowired:""` +} + +func (i *imlAIApiModule) APIs(ctx context.Context, keyword string, providerId string, start int64, end int64, page int, pageSize int, sortCondition string, asc bool, models []string, serviceIds []string) ([]*ai_dto.APIItem, *ai_dto.Condition, int64, error) { + p, has := model_runtime.GetProvider(providerId) + if !has { + return nil, nil, 0, fmt.Errorf("ai provider not found") + } + sortRule := "desc" + if asc { + sortRule = "asc" + } + services, err := i.serviceService.ServiceListByKind(ctx, service.AIService) + if err != nil { + return nil, nil, 0, err + } + serviceItems := make([]*ai_dto.BasicInfo, 0, len(services)) + serviceTeamMap := make(map[string]string) + for _, s := range services { + serviceItems = append(serviceItems, &ai_dto.BasicInfo{ + Id: s.Id, + Name: s.Name, + }) + serviceTeamMap[s.Id] = s.Team + + } + + modelItems := utils.SliceToSlice(p.Models(), func(e model_runtime.IModel) *ai_dto.BasicInfo { + return &ai_dto.BasicInfo{ + Id: e.ID(), + Name: e.ID(), + } + }) + condition := &ai_dto.Condition{Services: serviceItems, Models: modelItems} + switch sortCondition { + default: + w := map[string]interface{}{ + "provider": providerId, + } + if len(models) > 0 { + w["model"] = models + } + if len(serviceIds) > 0 { + w["service"] = serviceIds + } + apis, err := i.aiAPIService.Search(ctx, keyword, w, "update_at desc") + if err != nil { + return nil, nil, 0, err + } + + if len(apis) <= 0 { + return nil, condition, 0, nil + } + apiMap := make(map[string]*ai_api.API) + apiIds := make([]string, 0, len(apis)) + for _, a := range apis { + apiMap[a.ID] = a + apiIds = append(apiIds, a.ID) + } + offset := (page - 1) * pageSize + results, _, err := i.aiAPIUseService.SumByApisPage(ctx, providerId, start, end, offset, pageSize, fmt.Sprintf("total_token %s", sortRule), apiIds...) + if err != nil { + return nil, nil, 0, err + } + + apiItems := utils.SliceToSlice(results, func(e *ai_api.APIUse) *ai_dto.APIItem { + info := apiMap[e.API] + + delete(apiMap, e.API) + return &ai_dto.APIItem{ + Id: e.API, + Name: info.Name, + Service: auto.UUID(info.Service), + Team: auto.UUID(serviceTeamMap[info.Service]), + Method: http.MethodPost, + RequestPath: info.Path, + Model: auto.Label{ + Id: info.Model, + Name: info.Model, + }, + UpdateTime: auto.TimeLabel(info.UpdateAt), + UseToken: e.TotalToken, + Disable: info.Disable, + } + }) + sortApis := make([]*ai_dto.APIItem, 0, len(apiMap)) + for _, a := range apiMap { + sortApis = append(sortApis, &ai_dto.APIItem{ + Id: a.ID, + Name: a.Name, + Service: auto.UUID(a.Service), + Team: auto.UUID(serviceTeamMap[a.Service]), + Method: http.MethodPost, + RequestPath: a.Path, + Model: auto.Label{ + Id: a.Model, + Name: a.Model, + }, + UpdateTime: auto.TimeLabel(a.UpdateAt), + UseToken: 0, + Disable: a.Disable, + }) + } + // 排序 + sort.Slice(sortApis, func(i, j int) bool { + return time.Time(sortApis[i].UpdateTime).After(time.Time(sortApis[j].UpdateTime)) + }) + size := pageSize - len(apiItems) + for i := offset; i < offset+size && i < len(sortApis); i++ { + apiItems = append(apiItems, sortApis[i]) + } + + total := int64(len(apis)) + return apiItems, condition, total, nil + } +} diff --git a/module/ai/module.go b/module/ai/module.go index 4bc67358..99b6a56e 100644 --- a/module/ai/module.go +++ b/module/ai/module.go @@ -2,20 +2,29 @@ package ai import ( "context" + "reflect" + "github.com/APIParkLab/APIPark/gateway" ai_dto "github.com/APIParkLab/APIPark/module/ai/dto" "github.com/eolinker/go-common/autowire" - "reflect" ) type IProviderModule interface { - Providers(ctx context.Context) ([]*ai_dto.ProviderItem, error) + ConfiguredProviders(ctx context.Context) ([]*ai_dto.ConfiguredProviderItem, *ai_dto.BackupProvider, error) + UnConfiguredProviders(ctx context.Context) ([]*ai_dto.ProviderItem, error) SimpleProviders(ctx context.Context) ([]*ai_dto.SimpleProviderItem, error) + SimpleConfiguredProviders(ctx context.Context) ([]*ai_dto.SimpleProviderItem, *ai_dto.BackupProvider, error) Provider(ctx context.Context, id string) (*ai_dto.Provider, error) + SimpleProvider(ctx context.Context, id string) (*ai_dto.SimpleProvider, error) LLMs(ctx context.Context, driver string) ([]*ai_dto.LLMItem, *ai_dto.ProviderItem, error) - UpdateProviderStatus(ctx context.Context, id string, enable bool) error + //UpdateProviderStatus(ctx context.Context, id string, enable bool) error UpdateProviderConfig(ctx context.Context, id string, input *ai_dto.UpdateConfig) error - UpdateProviderDefaultLLM(ctx context.Context, id string, input *ai_dto.UpdateLLM) error + //UpdateProviderDefaultLLM(ctx context.Context, id string, input *ai_dto.UpdateLLM) error + Sort(ctx context.Context, input *ai_dto.Sort) error +} + +type IAIAPIModule interface { + APIs(ctx context.Context, keyword string, providerId string, start int64, end int64, page int, pageSize int, sortCondition string, asc bool, models []string, services []string) ([]*ai_dto.APIItem, *ai_dto.Condition, int64, error) } func init() { @@ -24,4 +33,8 @@ func init() { gateway.RegisterInitHandleFunc(module.initGateway) return reflect.ValueOf(module) }) + + autowire.Auto[IAIAPIModule](func() reflect.Value { + return reflect.ValueOf(new(imlAIApiModule)) + }) } diff --git a/module/catalogue/iml.go b/module/catalogue/iml.go index 18b58def..c43de0c8 100644 --- a/module/catalogue/iml.go +++ b/module/catalogue/iml.go @@ -176,19 +176,6 @@ func (i *imlCatalogueModule) Subscribe(ctx context.Context, subscribeInfo *catal // 修改订阅表状态 subscribers, err := i.subscribeService.ListByApplication(ctx, subscribeInfo.Service, appId) if err != nil { - //if !errors.Is(err, gorm.ErrRecordNotFound) { - // return err - //} - //err = i.subscribeService.Create(ctx, &subscribe.CreateSubscribe{ - // Uuid: uuid.New().String(), - // Service: subscribeInfo.Service, - // Application: appId, - // ApplyStatus: status, - // From: subscribe.FromSubscribe, - //}) - //if err != nil { - // return err - //} return err } else { subscriberMap := utils.SliceToMap(subscribers, func(t *subscribe.Subscribe) string { diff --git a/module/cluster/dto/input.go b/module/cluster/dto/input.go index 9d690526..43036f37 100644 --- a/module/cluster/dto/input.go +++ b/module/cluster/dto/input.go @@ -17,12 +17,12 @@ package cluster_dto //type SaveMonitorConfig struct { // Driver string `json:"driver"` -// Config map[string]interface{} `json:"config"` +// DefaultConfig map[string]interface{} `json:"config"` //} //type MonitorConfig struct { // Driver string `json:"driver"` -// Config map[string]interface{} `json:"config"` +// DefaultConfig map[string]interface{} `json:"config"` //} //type MonitorPartition struct { diff --git a/module/publish/iml.go b/module/publish/iml.go index 0eb17754..4226ef1f 100644 --- a/module/publish/iml.go +++ b/module/publish/iml.go @@ -124,6 +124,7 @@ func (m *imlPublishModule) getProjectRelease(ctx context.Context, projectID stri Version: version, } apis := make([]*gateway.ApiRelease, 0, len(apiInfos)) + hasUpstream := len(upstreamCommitIds) > 0 for _, a := range apiInfos { apiInfo := &gateway.ApiRelease{ BasicItem: &gateway.BasicItem{ @@ -133,7 +134,10 @@ func (m *imlPublishModule) getProjectRelease(ctx context.Context, projectID stri }, Path: a.Path, Methods: a.Methods, - Service: a.Upstream, + //Service: a.Upstream, + } + if hasUpstream { + apiInfo.Service = a.Upstream } proxy, ok := proxyCommitMap[a.UUID] if ok { diff --git a/module/router/dto/input.go b/module/router/dto/input.go index 02fcbf10..593a3647 100644 --- a/module/router/dto/input.go +++ b/module/router/dto/input.go @@ -30,7 +30,7 @@ type Create struct { MatchRules []Match `json:"match"` Upstream string `json:"upstream"` Proxy *InputProxy `json:"proxy"` - Disable bool `json:"disable"` + Disable bool `json:"disabled"` } type InputProxy struct { @@ -69,7 +69,7 @@ type Edit struct { Methods *[]string `json:"methods"` Protocols *[]string `json:"protocols"` MatchRules *[]Match `json:"match"` - Disable *bool `json:"disable"` + Disable *bool `json:"disabled"` Upstream *string `json:"upstream"` } diff --git a/module/router/dto/output.go b/module/router/dto/output.go index 9f8276ed..50611785 100644 --- a/module/router/dto/output.go +++ b/module/router/dto/output.go @@ -15,7 +15,7 @@ type Item struct { Protocols []string `json:"protocols"` Path string `json:"request_path"` Description string `json:"description"` - Disable bool `json:"disable"` + Disable bool `json:"disabled"` Creator auto.Label `json:"creator" aolabel:"user"` Updater auto.Label `json:"updater" aolabel:"user"` CreateTime auto.TimeLabel `json:"create_time"` @@ -34,7 +34,7 @@ type Detail struct { SimpleDetail Proxy *Proxy `json:"proxy"` Protocols []string `json:"protocols"` - Disable bool `json:"disable"` + Disable bool `json:"disabled"` //Doc map[string]interface{} `json:"doc"` } diff --git a/module/service/iml.go b/module/service/iml.go index 377990bd..8edf26d3 100644 --- a/module/service/iml.go +++ b/module/service/iml.go @@ -92,11 +92,6 @@ func (i *imlServiceModule) ExportAll(ctx context.Context) ([]*service_dto.Export serviceTagMap[st.Sid] = append(serviceTagMap[st.Sid], tagMap[st.Tid].Name) } - //docMap, err := i.serviceDocService.Map(ctx, serviceIds...) - //if err != nil { - // return nil, err - //} - items := make([]*service_dto.ExportService, 0, len(services)) for _, s := range services { info := &service_dto.ExportService{ @@ -171,24 +166,6 @@ func (i *imlServiceModule) SearchMyServices(ctx context.Context, teamId string, return items, nil } -//func (i *imlServiceModule) SimpleAPPS(ctx context.Context, keyword string) ([]*service_dto.SimpleServiceItem, error) { -// w := make(map[string]interface{}) -// w["as_app"] = true -// services, err := i.serviceService.SearchByDriver(ctx, keyword, w) -// if err != nil { -// return nil, err -// } -// return utils.SliceToSlice(services, func(p *service.Service) *service_dto.SimpleServiceItem { -// return &service_dto.SimpleServiceItem{ -// Id: p.Id, -// Name: p.Name, -// Description: p.Description, -// -// Team: auto.UUID(p.Team), -// } -// }), nil -//} - func (i *imlServiceModule) Simple(ctx context.Context) ([]*service_dto.SimpleServiceItem, error) { w := make(map[string]interface{}) w["as_server"] = true diff --git a/module/subscribe/iml.go b/module/subscribe/iml.go index 70e43bd2..03dc7935 100644 --- a/module/subscribe/iml.go +++ b/module/subscribe/iml.go @@ -218,33 +218,46 @@ func (i *imlSubscribeModule) AddSubscriber(ctx context.Context, serviceId string if err != nil { return err } - _, err = i.subscribeService.GetByServiceAndApplication(ctx, serviceId, input.Application) - if err == nil { - // 订阅方已存在 - return fmt.Errorf("subscriber is already exists") + clusters, err := i.clusterService.List(ctx) + if err != nil { + return err } - sub := &gateway.SubscribeRelease{ Service: serviceId, Application: input.Application, Expired: "0", } - clusters, err := i.clusterService.List(ctx) - if err != nil { - return err - } - return i.transaction.Transaction(ctx, func(ctx context.Context) error { - err = i.subscribeService.Create(ctx, &subscribe.CreateSubscribe{ - Uuid: uuid.New().String(), - Service: serviceId, - Application: input.Application, - ApplyStatus: subscribe.ApplyStatusSubscribe, - From: subscribe.FromUser, - }) - if err != nil { - return err + info, err := i.subscribeService.GetByServiceAndApplication(ctx, serviceId, input.Application) + if err == nil { + // 订阅方已存在 + if info.ApplyStatus != subscribe.ApplyStatusSubscribe { + // 更新订阅方状态 + status := subscribe.ApplyStatusSubscribe + from := subscribe.FromUser + err = i.subscribeService.Save(ctx, info.Id, &subscribe.UpdateSubscribe{ + ApplyStatus: &status, + From: &from, + }) + if err != nil { + return err + } + } else { + return nil + } + } else { + err = i.subscribeService.Create(ctx, &subscribe.CreateSubscribe{ + Uuid: uuid.New().String(), + Service: serviceId, + Application: input.Application, + ApplyStatus: subscribe.ApplyStatusSubscribe, + From: subscribe.FromUser, + }) + if err != nil { + return err + } } + for _, c := range clusters { err = i.onlineSubscriber(ctx, c.Uuid, sub) if err != nil { diff --git a/plugins/core/ai.go b/plugins/core/ai.go index c73e8173..e42a09db 100644 --- a/plugins/core/ai.go +++ b/plugins/core/ai.go @@ -10,13 +10,29 @@ import ( func (p *plugin) aiAPIs() []pm3.Api { return []pm3.Api{ - pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/ai/providers", []string{"context"}, []string{"providers"}, p.aiProviderController.Providers, access.SystemSettingsAiProviderView), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/ai/providers/unconfigured", []string{"context"}, []string{"providers"}, p.aiProviderController.UnConfiguredProviders, access.SystemSettingsAiProviderView), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/ai/providers/configured", []string{"context"}, []string{"providers", "backup"}, p.aiProviderController.ConfiguredProviders, access.SystemSettingsAiProviderView), pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/ai/providers", []string{"context"}, []string{"providers"}, p.aiProviderController.SimpleProviders), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/ai/providers/configured", []string{"context"}, []string{"providers", "backup"}, p.aiProviderController.SimpleConfiguredProviders), pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/ai/provider/config", []string{"context", "query:provider"}, []string{"provider"}, p.aiProviderController.Provider, access.SystemSettingsAiProviderView), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/ai/provider", []string{"context", "query:provider"}, []string{"provider"}, p.aiProviderController.SimpleProvider), pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/ai/provider/llms", []string{"context", "query:provider"}, []string{"llms", "provider"}, p.aiProviderController.LLMs), - //pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/ai/provider/enable", []string{"context", "query:provider"}, nil, p.aiProviderController.isStop), - //pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/ai/provider/disable", []string{"context", "query:provider"}, nil, p.aiProviderController.Disable), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/ai/provider/sort", []string{"context", "body"}, nil, p.aiProviderController.Sort), pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/ai/provider/config", []string{"context", "query:provider", "body"}, nil, p.aiProviderController.UpdateProviderConfig, access.SystemSettingsAiProviderManager), - //pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/ai/provider/default-llm", []string{"context", "query:provider", "body"}, nil, p.aiProviderController.UpdateProviderDefaultLLM), + + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/ai/apis", []string{"context", "query:keyword", "query:provider", "query:start", "query:end", "query:page", "query:page_size", "query:sort", "query:asc", "query:models", "query:services"}, []string{"apis", "condition", "total"}, p.aiStatisticController.APIs), + } +} + +func (p *plugin) aiKeyApis() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/ai/resource/key", []string{"context", "query:provider", "query:id"}, []string{"info"}, p.aiKeyController.Get), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/ai/resource/keys", []string{"context", "query:provider", "query:keyword", "query:page", "query:page_size", "query:statuses"}, []string{"keys", "total"}, p.aiKeyController.List), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/ai/resource/key", []string{"context", "query:provider", "body"}, nil, p.aiKeyController.Create), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/ai/resource/key", []string{"context", "query:provider", "query:id", "body"}, nil, p.aiKeyController.Edit), + pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/ai/resource/key", []string{"context", "query:provider", "query:id"}, nil, p.aiKeyController.Delete), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/ai/resource/key/enable", []string{"context", "query:provider", "query:id"}, nil, p.aiKeyController.Enable), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/ai/resource/key/disable", []string{"context", "query:provider", "query:id"}, nil, p.aiKeyController.Disable), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/ai/resource/key/sort", []string{"context", "query:provider", "body"}, nil, p.aiKeyController.Sort), } } diff --git a/plugins/core/core.go b/plugins/core/core.go index 7a3e57d6..55d8c3f2 100644 --- a/plugins/core/core.go +++ b/plugins/core/core.go @@ -3,6 +3,8 @@ package core import ( "net/http" + ai_key "github.com/APIParkLab/APIPark/controller/ai-key" + "github.com/APIParkLab/APIPark/controller/log" "github.com/APIParkLab/APIPark/controller/strategy" @@ -74,6 +76,8 @@ type plugin struct { upstreamController upstream.IUpstreamController `autowired:""` routerController router.IRouterController `autowired:""` aiAPIController ai_api.IAPIController `autowired:""` + aiStatisticController ai.IStatisticController `autowired:""` + aiKeyController ai_key.IKeyController `autowired:""` apiDocController router.IAPIDocController `autowired:""` subscribeController subscribe.ISubscribeController `autowired:""` strategyController strategy.IStrategyController `autowired:""` @@ -111,6 +115,7 @@ func (p *plugin) OnComplete() { p.apis = append(p.apis, p.commonApis()...) p.apis = append(p.apis, p.systemApis()...) p.apis = append(p.apis, p.aiAPIs()...) + p.apis = append(p.apis, p.aiKeyApis()...) p.apis = append(p.apis, p.strategyApis()...) p.apis = append(p.apis, p.logApis()...) } diff --git a/plugins/openapi/authorization.go b/plugins/openapi/authorization.go new file mode 100644 index 00000000..d6f37e8a --- /dev/null +++ b/plugins/openapi/authorization.go @@ -0,0 +1,18 @@ +package openapi + +import ( + "net/http" + + "github.com/eolinker/go-common/pm3" +) + +func (p *plugin) appAuthorizationApis() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodPost, "/openapi/v1/app/authorization", []string{"context", "query:app", "body"}, []string{"authorization"}, p.authorizationController.AddAuthorization), + pm3.CreateApiWidthDoc(http.MethodPut, "/openapi/v1/app/authorization", []string{"context", "query:app", "query:authorization", "body"}, []string{"authorization"}, p.authorizationController.EditAuthorization), + pm3.CreateApiWidthDoc(http.MethodDelete, "/openapi/v1/app/authorization", []string{"context", "query:app", "query:authorization"}, nil, p.authorizationController.DeleteAuthorization), + pm3.CreateApiWidthDoc(http.MethodGet, "/openapi/v1/app/authorization", []string{"context", "query:app", "query:authorization"}, []string{"authorization"}, p.authorizationController.Info), + pm3.CreateApiWidthDoc(http.MethodGet, "/openapi/v1/app/authorizations", []string{"context", "query:app"}, []string{"authorizations"}, p.authorizationController.Authorizations), + pm3.CreateApiWidthDoc(http.MethodGet, "/openapi/v1/app/authorization/details", []string{"context", "query:app", "query:authorization"}, []string{"details"}, p.authorizationController.Detail), + } +} diff --git a/plugins/openapi/check.go b/plugins/openapi/check.go new file mode 100644 index 00000000..4f5f50f7 --- /dev/null +++ b/plugins/openapi/check.go @@ -0,0 +1,45 @@ +package openapi + +import ( + "strings" + + "github.com/eolinker/eosc/env" + + "github.com/gin-gonic/gin" +) + +var ( + defaultAPIKey = "37eb0ebf" + openCheck = newOpenapiCheck() +) + +type openapiCheck struct { + apikey string +} + +func newOpenapiCheck() *openapiCheck { + apikey, has := env.GetEnv("API_KEY") + if !has { + apikey = defaultAPIKey + } + return &openapiCheck{apikey: apikey} +} + +func (o *openapiCheck) Check(method string, path string) (bool, []gin.HandlerFunc) { + if strings.HasPrefix(path, "/openapi/") { + return true, []gin.HandlerFunc{o.Handler} + } + return false, nil +} + +func (o *openapiCheck) Sort() int { + return -1 +} + +func (o *openapiCheck) Handler(ginCtx *gin.Context) { + authorization := ginCtx.GetHeader("Authorization") + if authorization == "" { + ginCtx.AbortWithStatusJSON(403, gin.H{"code": -8, "msg": "invalid token", "success": "fail"}) + return + } +} diff --git a/plugins/openapi/driver.go b/plugins/openapi/driver.go new file mode 100644 index 00000000..9e9997a6 --- /dev/null +++ b/plugins/openapi/driver.go @@ -0,0 +1,19 @@ +package openapi + +import ( + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/pm3" +) + +func init() { + pm3.Register("openapi", new(Driver)) +} + +type Driver struct { +} + +func (d *Driver) Create() (pm3.IPlugin, error) { + p := new(plugin) + autowire.Autowired(p) + return p, nil +} diff --git a/plugins/openapi/plugin.go b/plugins/openapi/plugin.go new file mode 100644 index 00000000..3082fb84 --- /dev/null +++ b/plugins/openapi/plugin.go @@ -0,0 +1,33 @@ +package openapi + +import ( + application_authorization "github.com/APIParkLab/APIPark/controller/application-authorization" + "github.com/eolinker/go-common/pm3" +) + +var ( + _ pm3.IPlugin = (*plugin)(nil) + _ pm3.IPluginMiddleware = (*plugin)(nil) +) + +type plugin struct { + apis []pm3.Api + authorizationController application_authorization.IAuthorizationController `autowired:""` +} + +func (p *plugin) Middlewares() []pm3.IMiddleware { + return []pm3.IMiddleware{ + openCheck, + } +} + +func (p *plugin) APis() []pm3.Api { + return p.apis +} + +func (p *plugin) Name() string { + return "openapi" +} +func (p *plugin) OnComplete() { + p.apis = p.appAuthorizationApis() +} diff --git a/readme/readme-jp.md b/readme/readme-jp.md index 6977a313..7f1602d0 100644 --- a/readme/readme-jp.md +++ b/readme/readme-jp.md @@ -211,7 +211,7 @@ APIParkはApache 2.0ライセンスの下で提供されています。詳細に エンタープライズ機能や専門的な技術サポートについては、プリセールスの専門家に連絡し、個別デモ、カスタムソリューション、価格情報を入手してください。 - ウェブサイト: https://apipark.com -- メール: dev@apipark.com +- メール: contact@apipark.com
diff --git a/readme/readme-zh-cn.md b/readme/readme-zh-cn.md index 9384a30a..ae7c9e8d 100644 --- a/readme/readme-zh-cn.md +++ b/readme/readme-zh-cn.md @@ -215,7 +215,7 @@ APIPark 使用 Apache 2.0 许可证。更多详情请查看 LICENSE 文件。 对于企业级功能和专业技术支持,请联系售前专家进行个性化演示、定制方案和获取报价。 - 网站: https://apipark.com -- 电子邮件: dev@apipark.com +- 电子邮件: contact@apipark.com
diff --git a/readme/readme-zh-tw.md b/readme/readme-zh-tw.md index c6ddc025..ab4dd255 100644 --- a/readme/readme-zh-tw.md +++ b/readme/readme-zh-tw.md @@ -212,7 +212,7 @@ APIPark 使用 Apache 2.0 授權條款。更多詳情請參閱 LICENSE 文件。 如需企業級功能與專業技術支援,請聯絡我們的售前專家,獲取個性化演示、定制方案和報價。 - 網站: https://apipark.com -- 電子郵件: dev@apipark.com +- 電子郵件: contact@apipark.com
diff --git a/resources/access/access.yaml b/resources/access/access.yaml index 6b24896f..504b01d1 100644 --- a/resources/access/access.yaml +++ b/resources/access/access.yaml @@ -100,6 +100,36 @@ system: value: 'manager' dependents: - system.settings.ai_provider.view + - name: ai key resource + value: 'ai_key_resource' + children: + - name: view + value: 'view' + guest_allow: true + - name: manager + value: 'manager' + dependents: + - system.settings.ai_key_resource.view + - name: ai api + value: 'ai_api' + children: + - name: view + value: 'view' + guest_allow: true + - name: manager + value: 'manager' + dependents: + - system.settings.ai_api.view + - name: ai log + value: 'ai_log' + children: + - name: view + value: 'view' + guest_allow: true + - name: manager + value: 'manager' + dependents: + - system.settings.ai_log.view - name: ssl certificate cname: 证书 value: 'ssl_certificate' diff --git a/resources/access/role.yaml b/resources/access/role.yaml index 9b9613db..b9f3ebe4 100644 --- a/resources/access/role.yaml +++ b/resources/access/role.yaml @@ -6,6 +6,12 @@ system: - system.api_portal.api_portal.view - system.settings.account.manager - system.settings.account.view + - system.settings.ai_api.manager + - system.settings.ai_api.view + - system.settings.ai_key_resource.manager + - system.settings.ai_key_resource.view + - system.settings.ai_log.manager + - system.settings.ai_log.view - system.settings.ai_provider.manager - system.settings.ai_provider.view - system.settings.api_gateway.manager @@ -39,6 +45,12 @@ system: permits: - system.analysis.run_view.view - system.api_portal.api_portal.view + - system.settings.ai_api.manager + - system.settings.ai_api.view + - system.settings.ai_key_resource.manager + - system.settings.ai_key_resource.view + - system.settings.ai_log.manager + - system.settings.ai_log.view - system.settings.ai_provider.manager - system.settings.ai_provider.view - system.settings.api_gateway.manager diff --git a/resources/plugin/plugin.yml b/resources/plugin/plugin.yml index f731a53f..73aa40bb 100644 --- a/resources/plugin/plugin.yml +++ b/resources/plugin/plugin.yml @@ -1,4 +1,4 @@ -version: v7 +version: v8 sort: - "access_log" - "monitor" @@ -41,7 +41,7 @@ plugin: b: "subscription_service:#{application}" response: status_code: 403 - content_typ: "text/plan" + content_type: "text/plan" charset: "utf-8" body: "Forbidden" diff --git a/scripts/Dockerfile b/scripts/Dockerfile index f78900c8..6e712a47 100755 --- a/scripts/Dockerfile +++ b/scripts/Dockerfile @@ -8,6 +8,9 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone ARG APP +ENV NSQ_ADDR=${APP}-nsq:4150 +ENV NSQ_TOPIC_PREFIX=${APP} + RUN mkdir -p /${APP} COPY cmd/* /${APP}/ diff --git a/scripts/build.sh b/scripts/build.sh index 47747583..0b6c85dc 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -8,6 +8,8 @@ source ./scripts/common.sh OUTPUT_DIR=$(mkdir_output "$1") APP="apipark" OUTPUT_BIN="${OUTPUT_DIR}/${APP}" +AI_EVENT_LISTEN_APP="apipark_ai_event_listen" +AI_EVENT_LISTEN_BIN="${OUTPUT_DIR}/${AI_EVENT_LISTEN_APP}" VERSION=$(gen_version "$2") BUILD_TYPE=$3 ARCH=$4 @@ -104,6 +106,10 @@ build_backend() { # -ldflags="-w -s" means omit DWARF symbol table and the symbol table and debug information echo "GOOS=linux GOARCH=$ARCH CGO_ENABLED=0 go build $Tags -ldflags \"-w -s $flags\" -o \"${OUTPUT_BIN}\"" GOOS=linux GOARCH=$ARCH CGO_ENABLED=0 go build ${Tags} -ldflags "-w -s $flags" -o ${OUTPUT_BIN} + + echo "Build backend successfully..." + echo "GOOS=linux GOARCH=$ARCH CGO_ENABLED=0 go build -ldflags \"-w -s\" -o \"${AI_EVENT_LISTEN_BIN}\" ./app/ai-event-handler" + GOOS=linux GOARCH=$ARCH CGO_ENABLED=0 go build -ldflags "-w -s" -o "${AI_EVENT_LISTEN_BIN}" ./app/ai-event-handler return } @@ -123,6 +129,9 @@ package() { cp "${OUTPUT_BIN}" "${PACKAGE_DIR}" + echo "cp ${AI_EVENT_LISTEN_BIN} ${PACKAGE_DIR}" + cp "${AI_EVENT_LISTEN_BIN}" "${PACKAGE_DIR}" + echo "tar -czvf ${PACKAGE_DIR}_linux_${ARCH}.tar.gz -C ${PACKAGE_DIR}/ ./" tar -czvf "${PACKAGE_DIR}_linux_${ARCH}.tar.gz" -C "${PACKAGE_DIR}/" "./" # rm -fr "${PACKAGE_DIR}" diff --git a/scripts/docker_build.sh b/scripts/docker_build.sh index 2a43ba84..5c51a097 100755 --- a/scripts/docker_build.sh +++ b/scripts/docker_build.sh @@ -17,7 +17,7 @@ source ./scripts/common.sh APP="apipark" -mkdir -p scripts/cmd/ && cp cmd/${APP} scripts/cmd/ +mkdir -p scripts/cmd/ && cp cmd/${APP} scripts/cmd/ && cp cmd/apipark_ai_event_listen scripts/cmd/ VERSION=$(gen_version) diff --git a/scripts/resource/docker_run.sh b/scripts/resource/docker_run.sh index 53d3d711..255f7243 100755 --- a/scripts/resource/docker_run.sh +++ b/scripts/resource/docker_run.sh @@ -25,6 +25,9 @@ for s in ${arr[@]} do echo -e " - $s" >> config.yml done +echo -e "nsq:" >> config.yml +echo -e " addr: ${NSQ_ADDR}" >> config.yml +echo -e " topic_prefix: ${NSQ_TOPIC_PREFIX}" >> config.yml echo -e "port: 8288" >> config.yml echo -e "error_log:" >> config.yml echo -e " dir: ${ERROR_DIR}" >> config.yml @@ -34,4 +37,6 @@ echo -e " log_expire: ${ERROR_EXPIRE}" >> config.yml echo -e " log_period: ${ERROR_PERIOD}" >> config.yml cat config.yml -./apipark \ No newline at end of file +nohup ./apipark >> run.log 2>&1 & +nohup ./apipark_ai_event_listen >> run.log 2>&1 & +tail -F run.log \ No newline at end of file diff --git a/scripts/resource/run.sh b/scripts/resource/run.sh index cc998bd9..6aff686b 100755 --- a/scripts/resource/run.sh +++ b/scripts/resource/run.sh @@ -42,7 +42,8 @@ start() { cat "$LOG_FILE" exit 1 fi - + # 启动ai事件监听程序 +# nohup ./apipark_ai_event_listen >> "$LOG_FILE" 2>&1 & } # 停止函数 diff --git a/service/ai-api/iml.go b/service/ai-api/iml.go index 4971dcbf..b59a004e 100644 --- a/service/ai-api/iml.go +++ b/service/ai-api/iml.go @@ -1,10 +1,15 @@ package ai_api import ( + "context" "encoding/json" + "errors" + "time" + + "gorm.io/gorm" + "github.com/APIParkLab/APIPark/service/universally" "github.com/APIParkLab/APIPark/stores/api" - "time" ) var _ IAPIService = (*imlAPIService)(nil) @@ -18,6 +23,10 @@ type imlAPIService struct { universally.IServiceDelete } +func (i *imlAPIService) CountMapByProvider(ctx context.Context, keyword string, conditions map[string]interface{}) (map[string]int64, error) { + return i.store.CountByGroup(ctx, keyword, conditions, "provider") +} + func (i *imlAPIService) OnComplete() { i.IServiceGet = universally.NewGetSoftDelete[API, api.AiAPIInfo](i.store, FromEntity) i.IServiceCreate = universally.NewCreatorSoftDelete[Create, api.AiAPIInfo](i.store, "ai_api_info", createEntityHandler, uniquestHandler, labelHandler) @@ -26,7 +35,7 @@ func (i *imlAPIService) OnComplete() { } func labelHandler(e *api.AiAPIInfo) []string { - return []string{e.Name, e.Uuid} + return []string{e.Name, e.Path} } func uniquestHandler(i *Create) []map[string]interface{} { return []map[string]interface{}{{"uuid": i.ID}} @@ -43,6 +52,8 @@ func createEntityHandler(i *Create) *api.AiAPIInfo { Timeout: i.Timeout, Retry: i.Retry, Model: i.Model, + Provider: i.Provider, + Disable: i.Disable, CreateAt: now, UpdateAt: now, AdditionalConfig: string(cfg), @@ -67,9 +78,89 @@ func updateHandler(e *api.AiAPIInfo, i *Edit) { if i.Model != nil { e.Model = *i.Model } + if i.Provider != nil { + e.Provider = *i.Provider + } if i.AdditionalConfig != nil { cfg, _ := json.Marshal(i.AdditionalConfig) e.AdditionalConfig = string(cfg) } + if i.Disable != nil { + e.Disable = *i.Disable + } + if i.UseToken != nil { + e.UseToken = *i.UseToken + } e.UpdateAt = time.Now() } + +var _ IAPIUseService = (*imlAPIUseService)(nil) + +type imlAPIUseService struct { + store api.IAiAPIUseStore `autowired:""` +} + +func (i *imlAPIUseService) Incr(ctx context.Context, incr *IncrAPIUse) error { + info, err := i.store.First(ctx, map[string]interface{}{ + "api": incr.API, + "service": incr.Service, + "provider": incr.Provider, + "model": incr.Model, + "day": incr.Day, + "hour": incr.Hour, + "minute": incr.Minute, + }) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + info = &api.AiAPIUse{ + API: incr.API, + Service: incr.Service, + Provider: incr.Provider, + Model: incr.Model, + Day: incr.Day, + Hour: incr.Hour, + Minute: incr.Minute, + } + } + info.InputToken += incr.InputToken + info.OutputToken += incr.OutputToken + info.TotalToken += incr.TotalToken + return i.store.Save(ctx, info) +} + +func (i *imlAPIUseService) SumByApisPage(ctx context.Context, providerId string, start, end int64, offset, limit int, order string, apiIds ...string) ([]*APIUse, int64, error) { + list, total, err := i.store.SumByGroupPage(ctx, "api", order, offset, limit, "api,sum(input_token) as input_token,sum(output_token) as output_token,sum(total_token) as total_token", "provider = ? and api in (?) and minute >= ? and minute <= ?", providerId, apiIds, start, end) + if err != nil { + return nil, 0, err + } + + result := make([]*APIUse, 0, len(list)) + for _, v := range list { + result = append(result, &APIUse{ + API: v.API, + InputToken: v.InputToken, + OutputToken: v.OutputToken, + TotalToken: v.TotalToken, + }) + } + return result, total, nil +} + +func (i *imlAPIUseService) SumByApis(ctx context.Context, providerId string, start, end int64, apiIds ...string) ([]*APIUse, error) { + //list, err := i.store.SumByGroup(ctx, "api", "api,sum(input_token) as input_token,sum(output_token) as output_token,sum(total_token) as total_token", "provider = ? and api in (?) and minute >= ? and minute <= ?", providerId, apiIds, start, end) + //if err != nil { + // return nil, err + //} + // + //return utils.SliceToSlice(list, func(v *api.AiAPIUse) *APIUse { + // return &APIUse{ + // API: v.API, + // InputToken: v.InputToken, + // OutputToken: v.OutputToken, + // TotalToken: v.TotalToken, + // } + //}), nil + return nil, nil +} diff --git a/service/ai-api/model.go b/service/ai-api/model.go index bf9e7d41..b8ee9cb7 100644 --- a/service/ai-api/model.go +++ b/service/ai-api/model.go @@ -16,8 +16,10 @@ type API struct { Timeout int Retry int Model string + Provider string CreateAt time.Time UpdateAt time.Time + UseToken int Creator string Updater string AdditionalConfig map[string]interface{} @@ -33,7 +35,9 @@ type Create struct { Timeout int Retry int Model string + Provider string AdditionalConfig map[string]interface{} + Disable bool } type Edit struct { @@ -42,7 +46,10 @@ type Edit struct { Description *string Timeout *int Retry *int + Provider *string Model *string + Disable *bool + UseToken *int AdditionalConfig *map[string]interface{} } @@ -64,6 +71,28 @@ func FromEntity(e *api.AiAPIInfo) *API { UpdateAt: e.UpdateAt, Creator: e.Creator, Updater: e.Updater, + Disable: e.Disable, + UseToken: e.UseToken, AdditionalConfig: cfg, } } + +type APIUse struct { + API string + InputToken int + OutputToken int + TotalToken int +} + +type IncrAPIUse struct { + API string + Service string + Provider string + Model string + Day int64 + Hour int64 + Minute int64 + InputToken int + OutputToken int + TotalToken int +} diff --git a/service/ai-api/service.go b/service/ai-api/service.go index 6cf068ee..de841fda 100644 --- a/service/ai-api/service.go +++ b/service/ai-api/service.go @@ -1,9 +1,11 @@ package ai_api import ( + "context" + "reflect" + "github.com/APIParkLab/APIPark/service/universally" "github.com/eolinker/go-common/autowire" - "reflect" ) type IAPIService interface { @@ -11,12 +13,20 @@ type IAPIService interface { universally.IServiceCreate[Create] universally.IServiceEdit[Edit] universally.IServiceDelete + CountMapByProvider(ctx context.Context, keyword string, conditions map[string]interface{}) (map[string]int64, error) +} - //ListByServices(ctx context.Context, serviceIds ...string) ([]*API, error) +type IAPIUseService interface { + SumByApis(ctx context.Context, providerId string, start, end int64, apiIds ...string) ([]*APIUse, error) + SumByApisPage(ctx context.Context, providerId string, start, end int64, page, pageSize int, order string, apiIds ...string) ([]*APIUse, int64, error) + Incr(ctx context.Context, incr *IncrAPIUse) error } func init() { autowire.Auto[IAPIService](func() reflect.Value { return reflect.ValueOf(new(imlAPIService)) }) + autowire.Auto[IAPIUseService](func() reflect.Value { + return reflect.ValueOf(new(imlAPIUseService)) + }) } diff --git a/service/ai-key/iml.go b/service/ai-key/iml.go new file mode 100644 index 00000000..302ec182 --- /dev/null +++ b/service/ai-key/iml.go @@ -0,0 +1,244 @@ +package ai_key + +import ( + "context" + "fmt" + "sort" + "time" + + "github.com/eolinker/go-common/store" + + "github.com/APIParkLab/APIPark/service/universally" + "github.com/APIParkLab/APIPark/stores/ai" +) + +var _ IKeyService = &imlAIKeyService{} + +type imlAIKeyService struct { + store ai.IKeyStore `autowired:""` + transaction store.ITransaction `autowired:""` + universally.IServiceGet[Key] + universally.IServiceCreate[Create] + universally.IServiceEdit[Edit] + universally.IServiceDelete +} + +func (i *imlAIKeyService) IncrUseToken(ctx context.Context, id string, useToken int) error { + info, err := i.store.GetByUUID(ctx, id) + if err != nil { + return err + } + + info.UseToken += useToken + return i.store.Save(ctx, info) +} + +func (i *imlAIKeyService) SearchUnExpiredByPage(ctx context.Context, w map[string]interface{}, page, pageSize int, order string) ([]*Key, int64, error) { + sql := "(expire_time = 0 || expire_time > ?)" + args := []interface{}{time.Now().Unix()} + for k, v := range w { + switch v.(type) { + case []int: + sql += fmt.Sprintf(" and `%s` in (?)", k) + default: + sql += fmt.Sprintf(" and `%s` = ?", k) + } + args = append(args, v) + } + list, total, err := i.store.ListPage(ctx, sql, page, pageSize, args, order) + if err != nil { + return nil, 0, err + } + var result []*Key + for _, item := range list { + result = append(result, FromEntity(item)) + } + return result, total, nil +} + +func (i *imlAIKeyService) KeysAfterPriority(ctx context.Context, providerId string, priority int) ([]*Key, error) { + list, err := i.store.ListQuery(ctx, "sort > ? and provider = ?", []interface{}{priority, providerId}, "sort asc") + if err != nil { + return nil, err + } + var result []*Key + for _, item := range list { + result = append(result, FromEntity(item)) + } + return result, nil +} + +func (i *imlAIKeyService) MaxPriority(ctx context.Context, providerId string) (int, error) { + info, err := i.store.First(ctx, map[string]interface{}{"provider": providerId}, "sort desc") + if err != nil { + return 0, err + } + return info.Sort, nil +} + +func (i *imlAIKeyService) DefaultKey(ctx context.Context, providerId string) (*Key, error) { + info, err := i.store.First(ctx, map[string]interface{}{"provider": providerId, "default": true}) + if err != nil { + return nil, err + } + return FromEntity(info), nil +} + +func (i *imlAIKeyService) KeysByProvider(ctx context.Context, providerId string) ([]*Key, error) { + list, err := i.store.List(ctx, map[string]interface{}{"provider": providerId}) + if err != nil { + return nil, err + } + var result []*Key + for _, item := range list { + result = append(result, FromEntity(item)) + } + return result, nil +} + +func (i *imlAIKeyService) SortBefore(ctx context.Context, provider string, originID string, targetID string) ([]*Key, error) { + originKey, err := i.store.GetByUUID(ctx, originID) + if err != nil { + return nil, fmt.Errorf("get key error: %v,id is %s", err, originID) + } + targetKey, err := i.store.GetByUUID(ctx, targetID) + if err != nil { + return nil, fmt.Errorf("get key error: %v,id is %s", err, targetID) + } + originKeySort, targetKeySort := originKey.Sort, targetKey.Sort + // 初始化顺序,假设原始Key在目标Key之后,中间的key往后移动,原始Key移动到`targetKeySort`位置 + originKey.Sort = targetKeySort + fn := func(priority int) int { + return priority + 1 + } + sql := "sort < ? and sort >= ?" + if originKeySort < targetKeySort { + // 如果原始Key在目标Key之前,中间的key往前移动,原始Key移动到`targetKeySort - 1`位置 + sql = "sort > ? and sort < ?" + originKey.Sort = targetKeySort - 1 + fn = func(priority int) int { + return priority - 1 + } + } + list, err := i.store.ListQuery(ctx, sql, []interface{}{originKeySort, targetKeySort}, "sort asc") + if err != nil { + return nil, err + } + result := make([]*Key, 0, len(list)+1) + err = i.transaction.Transaction(ctx, func(txCtx context.Context) error { + for _, l := range list { + l.Sort = fn(l.Sort) + _, err := i.store.Update(ctx, l) + if err != nil { + return err + } + result = append(result, FromEntity(l)) + } + _, err = i.store.Update(ctx, originKey) + return err + }) + if err != nil { + return nil, err + } + result = append(result, FromEntity(originKey)) + sort.Slice(list, func(i, j int) bool { return list[i].Sort < list[j].Sort }) + return result, nil +} + +func (i *imlAIKeyService) SortAfter(ctx context.Context, provider string, originID string, targetID string) ([]*Key, error) { + originKey, err := i.store.GetByUUID(ctx, originID) + if err != nil { + return nil, fmt.Errorf("get key error: %v,id is %s", err, originID) + } + targetKey, err := i.store.GetByUUID(ctx, targetID) + if err != nil { + return nil, fmt.Errorf("get key error: %v,id is %s", err, targetID) + } + originKeySort, targetKeySort := originKey.Sort, targetKey.Sort + // 初始化顺序,假设原始Key在目标Key之后,中间的Key往后移动,原始Key移动到`targetKeySort + 1`位置 + originKey.Sort = targetKeySort + 1 + fn := func(priority int) int { + return priority + 1 + } + sql := "sort < ? and sort > ?" + if originKeySort < targetKeySort { + // 如果原始Key在目标Key之前,中间的Key往前移动,原始Key移动到`targetKeySort`位置 + sql = "sort > ? and sort <= ?" + originKey.Sort = targetKeySort + fn = func(priority int) int { + return priority - 1 + } + } + list, err := i.store.ListQuery(ctx, sql, []interface{}{originKeySort, targetKeySort}, "sort asc") + if err != nil { + return nil, err + } + result := make([]*Key, 0, len(list)+1) + err = i.transaction.Transaction(ctx, func(txCtx context.Context) error { + for _, l := range list { + l.Sort = fn(l.Sort) + _, err := i.store.Update(ctx, l) + if err != nil { + return err + } + result = append(result, FromEntity(l)) + } + _, err = i.store.Update(ctx, originKey) + return err + }) + if err != nil { + return nil, err + } + result = append(result, FromEntity(originKey)) + sort.Slice(list, func(i, j int) bool { return list[i].Sort < list[j].Sort }) + return result, nil +} + +func (i *imlAIKeyService) OnComplete() { + i.IServiceGet = universally.NewGet[Key, ai.Key](i.store, FromEntity) + i.IServiceCreate = universally.NewCreator[Create, ai.Key](i.store, "ai_api_info", createEntityHandler, uniquestHandler, labelHandler) + i.IServiceEdit = universally.NewEdit[Edit, ai.Key](i.store, updateHandler) + i.IServiceDelete = universally.NewDelete[ai.Key](i.store) +} + +func labelHandler(e *ai.Key) []string { + return []string{e.Name} +} +func uniquestHandler(i *Create) []map[string]interface{} { + return []map[string]interface{}{{"uuid": i.ID}} +} +func createEntityHandler(i *Create) *ai.Key { + now := time.Now() + return &ai.Key{ + Uuid: i.ID, + Name: i.Name, + Config: i.Config, + Provider: i.Provider, + Status: i.Status, + ExpireTime: i.ExpireTime, + Sort: i.Priority, + UseToken: 0, + CreateAt: now, + UpdateAt: now, + Default: i.Default, + } +} +func updateHandler(e *ai.Key, i *Edit) { + if i.Name != nil { + e.Name = *i.Name + } + if i.Config != nil { + e.Config = *i.Config + } + if i.Status != nil { + e.Status = *i.Status + } + if i.ExpireTime != nil { + e.ExpireTime = *i.ExpireTime + } + if i.Priority != nil { + e.Sort = *i.Priority + } + + e.UpdateAt = time.Now() +} diff --git a/service/ai-key/model.go b/service/ai-key/model.go new file mode 100644 index 00000000..61148922 --- /dev/null +++ b/service/ai-key/model.go @@ -0,0 +1,61 @@ +package ai_key + +import ( + "time" + + "github.com/APIParkLab/APIPark/stores/ai" +) + +type Key struct { + ID string + Name string + Config string + Provider string + Status int + ExpireTime int + UseToken int + Creator string + Updater string + Priority int + CreateAt time.Time + UpdateAt time.Time + Default bool +} + +func FromEntity(e *ai.Key) *Key { + return &Key{ + ID: e.Uuid, + Name: e.Name, + Config: e.Config, + Provider: e.Provider, + Status: e.Status, + ExpireTime: e.ExpireTime, + UseToken: e.UseToken, + Creator: e.Creator, + Updater: e.Updater, + CreateAt: e.CreateAt, + UpdateAt: e.UpdateAt, + Priority: e.Sort, + Default: e.Default, + } +} + +type Create struct { + ID string + Name string + Config string + Provider string + Priority int + Status int + ExpireTime int + UseToken int + Default bool +} + +type Edit struct { + Name *string + Config *string + Status *int + ExpireTime *int + Priority *int +} diff --git a/service/ai-key/service.go b/service/ai-key/service.go new file mode 100644 index 00000000..8bbe6d83 --- /dev/null +++ b/service/ai-key/service.go @@ -0,0 +1,31 @@ +package ai_key + +import ( + "context" + "reflect" + + "github.com/eolinker/go-common/autowire" + + "github.com/APIParkLab/APIPark/service/universally" +) + +type IKeyService interface { + universally.IServiceGet[Key] + universally.IServiceCreate[Create] + universally.IServiceEdit[Edit] + universally.IServiceDelete + DefaultKey(ctx context.Context, providerId string) (*Key, error) + KeysByProvider(ctx context.Context, providerId string) ([]*Key, error) + MaxPriority(ctx context.Context, providerId string) (int, error) + SortBefore(ctx context.Context, provider string, originID string, targetID string) ([]*Key, error) + SortAfter(ctx context.Context, provider string, originID string, targetID string) ([]*Key, error) + KeysAfterPriority(ctx context.Context, providerId string, priority int) ([]*Key, error) + SearchUnExpiredByPage(ctx context.Context, w map[string]interface{}, page, pageSize int, order string) ([]*Key, int64, error) + IncrUseToken(ctx context.Context, id string, useToken int) error +} + +func init() { + autowire.Auto[IKeyService](func() reflect.Value { + return reflect.ValueOf(new(imlAIKeyService)) + }) +} diff --git a/service/ai/iml.go b/service/ai/iml.go index 2eea283f..453d63a7 100644 --- a/service/ai/iml.go +++ b/service/ai/iml.go @@ -4,12 +4,13 @@ import ( "context" "encoding/base64" "errors" + "time" + "github.com/APIParkLab/APIPark/service/universally" "github.com/APIParkLab/APIPark/stores/ai" "github.com/eolinker/go-common/auto" "github.com/eolinker/go-common/utils" "gorm.io/gorm" - "time" ) var _ IProviderService = (*imlProviderService)(nil) @@ -19,6 +20,14 @@ type imlProviderService struct { store ai.IProviderStore `autowired:""` } +func (i *imlProviderService) MaxPriority(ctx context.Context) (int, error) { + t, err := i.store.First(ctx, nil, "priority desc") + if err != nil { + return 0, err + } + return t.Priority, nil +} + func (i *imlProviderService) Save(ctx context.Context, id string, cfg *SetProvider) error { userId := utils.UserId(ctx) now := time.Now() @@ -30,10 +39,20 @@ func (i *imlProviderService) Save(ctx context.Context, id string, cfg *SetProvid if cfg.Name == nil || cfg.Config == nil || cfg.DefaultLLM == nil { return errors.New("invalid params") } - status := false + status := 1 if cfg.Status != nil { status = *cfg.Status } + priority := 1 + if cfg.Priority == nil { + count, err := i.store.Count(ctx, "", nil) + if err != nil { + return err + } + priority = int(count) + 1 + } else { + priority = *cfg.Priority + } info = &ai.Provider{ UUID: id, Name: *cfg.Name, @@ -42,6 +61,7 @@ func (i *imlProviderService) Save(ctx context.Context, id string, cfg *SetProvid Status: status, Creator: userId, Updater: userId, + Priority: priority, CreateAt: now, UpdateAt: now, } @@ -58,6 +78,9 @@ func (i *imlProviderService) Save(ctx context.Context, id string, cfg *SetProvid if cfg.Status != nil { info.Status = *cfg.Status } + if cfg.Priority != nil { + info.Priority = *cfg.Priority + } info.Updater = userId info.UpdateAt = now } diff --git a/service/ai/model.go b/service/ai/model.go index 6865f5bf..80c6b43f 100644 --- a/service/ai/model.go +++ b/service/ai/model.go @@ -2,8 +2,9 @@ package ai import ( "encoding/base64" - "github.com/APIParkLab/APIPark/stores/ai" "time" + + "github.com/APIParkLab/APIPark/stores/ai" ) type Provider struct { @@ -13,7 +14,8 @@ type Provider struct { Config string Creator string Updater string - Status bool + Status int + Priority int CreateAt time.Time UpdateAt time.Time } @@ -22,7 +24,8 @@ type SetProvider struct { Name *string DefaultLLM *string Config *string - Status *bool + Status *int + Priority *int } func FromEntity(e *ai.Provider) *Provider { @@ -40,5 +43,6 @@ func FromEntity(e *ai.Provider) *Provider { CreateAt: e.CreateAt, UpdateAt: e.UpdateAt, Status: e.Status, + Priority: e.Priority, } } diff --git a/service/ai/service.go b/service/ai/service.go index 39dd9c7b..83760314 100644 --- a/service/ai/service.go +++ b/service/ai/service.go @@ -2,14 +2,16 @@ package ai import ( "context" + "reflect" + "github.com/APIParkLab/APIPark/service/universally" "github.com/eolinker/go-common/autowire" - "reflect" ) type IProviderService interface { universally.IServiceGet[Provider] Save(ctx context.Context, id string, cfg *SetProvider) error + MaxPriority(ctx context.Context) (int, error) } func init() { diff --git a/service/service/iml.go b/service/service/iml.go index c3090ea0..719e55e7 100644 --- a/service/service/iml.go +++ b/service/service/iml.go @@ -46,7 +46,7 @@ func (i *imlServiceService) ServiceListByKind(ctx context.Context, kind Kind, se w["uuid"] = serviceIds } w["as_server"] = true - w["kind"] = kind + w["kind"] = kind.Int() w["is_delete"] = false list, err := i.store.List(ctx, w) if err != nil { diff --git a/service/subscribe/iml.go b/service/subscribe/iml.go index dd3fd460..92d0b4f6 100644 --- a/service/subscribe/iml.go +++ b/service/subscribe/iml.go @@ -200,6 +200,9 @@ func (i *imlSubscribeService) updateHandler(e *subscribe.Subscribe, t *UpdateSub if t.ApplyStatus != nil { e.ApplyStatus = *t.ApplyStatus } + if t.From != nil { + e.From = *t.From + } } var ( diff --git a/service/subscribe/model.go b/service/subscribe/model.go index d9882145..365a9d4d 100644 --- a/service/subscribe/model.go +++ b/service/subscribe/model.go @@ -2,14 +2,14 @@ package subscribe import ( "time" - + "github.com/APIParkLab/APIPark/stores/subscribe" ) type Subscribe struct { Id string Service string - + // 订阅方相关 Application string From int @@ -28,6 +28,7 @@ type CreateSubscribe struct { } type UpdateSubscribe struct { + From *int ApplyStatus *int } diff --git a/stores/access_log/model.go b/stores/access_log/model.go new file mode 100644 index 00000000..369ecef5 --- /dev/null +++ b/stores/access_log/model.go @@ -0,0 +1,30 @@ +package access_log + +import "time" + +type Log struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Cluster string `gorm:"column:cluster;type:varchar(36);NOT NULL;comment:集群ID"` + Node string `gorm:"column:node;type:varchar(36);NOT NULL;comment:节点ID"` + Service string `gorm:"column:service;type:varchar(36);NOT NULL;comment:服务ID"` + API string `gorm:"column:api;type:varchar(36);NOT NULL;comment:API ID"` + Application string `gorm:"column:application;type:varchar(36);NOT NULL;comment:应用ID"` + Auth string `gorm:"column:auth;type:varchar(36);NOT NULL;comment:认证ID"` + Type string `gorm:"column:type;type:varchar(36);NOT NULL;comment:日志类型;index:idx_type"` + Target string `gorm:"column:target;type:varchar(36);NOT NULL;comment:目标ID"` + IP string `gorm:"column:ip;type:varchar(36);NOT NULL;comment:IP"` + RequestPath string `gorm:"column:request_path;type:varchar(255);NOT NULL;comment:请求路径"` + Method string `gorm:"column:method;type:varchar(36);NOT NULL;comment:请求方法"` + ResponseTime float64 `gorm:"column:response_time;type:float;NOT NULL;comment:响应时间"` + StatusCode int64 `gorm:"column:status_code;type:BIGINT(20);NOT NULL;comment:响应状态码"` + ReportTime time.Time `gorm:"column:report_time;type:timestamp;NOT NULL;comment:上报时间;index:idx_report_time"` +} + +func (c *Log) IdValue() int64 { + return c.Id +} + +func (c *Log) TableName() string { + return "access_log" +} diff --git a/stores/access_log/store.go b/stores/access_log/store.go new file mode 100644 index 00000000..a702d7db --- /dev/null +++ b/stores/access_log/store.go @@ -0,0 +1,22 @@ +package access_log + +import ( + "reflect" + + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" +) + +type ILogStore interface { + store.ISearchStore[Log] +} + +type imlLogStore struct { + store.SearchStore[Log] +} + +func init() { + autowire.Auto[ILogStore](func() reflect.Value { + return reflect.ValueOf(new(imlLogStore)) + }) +} diff --git a/stores/ai/model.go b/stores/ai/model.go index 7b82e702..b25f4c29 100644 --- a/stores/ai/model.go +++ b/stores/ai/model.go @@ -8,7 +8,8 @@ type Provider struct { Name string `gorm:"type:varchar(100);not null;column:name;comment:name"` DefaultLLM string `gorm:"type:varchar(255);not null;column:default_llm;comment:默认模型ID"` Config string `gorm:"type:text;not null;column:config;comment:配置信息"` - Status bool `gorm:"type:tinyint(1);not null;column:status;comment:状态"` + Status int `gorm:"type:tinyint(1);not null;column:status;comment:状态,0:停用;1:启用,2:异常;default:1"` + Priority int `gorm:"type:int;not null;column:priority;comment:优先级,值越小优先级越高"` Creator string `gorm:"size:36;not null;column:creator;comment:创建人;index:creator" aovalue:"creator"` // 创建人 Updater string `gorm:"size:36;not null;column:updater;comment:更新人;index:updater" aovalue:"updater"` // 更新人 CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` @@ -22,3 +23,48 @@ func (i *Provider) TableName() string { func (i *Provider) IdValue() int64 { return i.Id } + +type LogMetrics struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Provider string `gorm:"type:varchar(36);not null;column:provider;comment:供应商ID"` + Model string `gorm:"type:varchar(36);not null;column:model;comment:模型ID"` + InputToken int `gorm:"type:int;not null;column:input_token;comment:输入token"` + OutputToken int `gorm:"type:int;not null;column:output_token;comment:输出token"` + TotalToken int `gorm:"type:int;not null;column:total_token;comment:总token"` + Cost float64 `gorm:"type:int;not null;column:cost;comment:费用"` + Per float64 `gorm:"type:int;not null;column:per;comment:每个token的价格"` +} + +func (i *LogMetrics) TableName() string { + return "ai_log_metrics" +} + +func (i *LogMetrics) IdValue() int64 { + return i.Id +} + +type Key struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + Uuid string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Name string `gorm:"type:varchar(100);not null;column:name;comment:名称"` + Config string `gorm:"type:text;not null;column:config;comment:配置"` + Provider string `gorm:"type:varchar(36);not null;column:provider;comment:供应商ID"` + Status int `gorm:"type:tinyint(1);not null;column:status;comment:状态,0:停用;1:启用,2:错误;3:超额;4:过期"` + ExpireTime int `gorm:"type:int;not null;column:expire_time;comment:过期时间"` + UseToken int `gorm:"type:int;not null;column:use_token;comment:使用token数"` + Sort int `gorm:"type:int;not null;column:sort;comment:排序"` + Creator string `gorm:"size:36;not null;column:creator;comment:创建人;index:creator" aovalue:"creator"` // 创建人 + Updater string `gorm:"size:36;not null;column:updater;comment:更新人;index:updater" aovalue:"updater"` // 更新人 + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` + UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:update_at;comment:更新时间"` + Default bool `gorm:"type:tinyint(1);not null;column:default;comment:是否默认"` +} + +func (i *Key) TableName() string { + return "ai_key" +} + +func (i *Key) IdValue() int64 { + return i.Id +} diff --git a/stores/ai/store.go b/stores/ai/store.go index 107f625c..4afa193f 100644 --- a/stores/ai/store.go +++ b/stores/ai/store.go @@ -1,9 +1,10 @@ package ai import ( + "reflect" + "github.com/eolinker/go-common/autowire" "github.com/eolinker/go-common/store" - "reflect" ) type IProviderStore interface { @@ -14,8 +15,32 @@ type imlProviderStore struct { store.SearchStore[Provider] } +type ILogMetricsStore interface { + store.ISearchStore[LogMetrics] +} + +type imlLogMetricsStore struct { + store.SearchStore[LogMetrics] +} + +type IKeyStore interface { + store.ISearchStore[Key] +} + +type imlKeyStore struct { + store.SearchStore[Key] +} + func init() { autowire.Auto[IProviderStore](func() reflect.Value { return reflect.ValueOf(new(imlProviderStore)) }) + + autowire.Auto[ILogMetricsStore](func() reflect.Value { + return reflect.ValueOf(new(imlLogMetricsStore)) + }) + + autowire.Auto[IKeyStore](func() reflect.Value { + return reflect.ValueOf(new(imlKeyStore)) + }) } diff --git a/stores/api/api.go b/stores/api/api.go index 4eab2d83..26fd5ba4 100644 --- a/stores/api/api.go +++ b/stores/api/api.go @@ -31,6 +31,14 @@ type imlAiAPIInfoStore struct { store.SearchStoreSoftDelete[AiAPIInfo] } +type IAiAPIUseStore interface { + store.IStatisticsStore[AiAPIUse] +} + +type imlAiAPIUseStore struct { + store.StatisticsStore[AiAPIUse] +} + func init() { autowire.Auto[IApiBaseStore](func() reflect.Value { @@ -48,4 +56,8 @@ func init() { autowire.Auto[IAiAPIInfoStore](func() reflect.Value { return reflect.ValueOf(new(imlAiAPIInfoStore)) }) + + autowire.Auto[IAiAPIUseStore](func() reflect.Value { + return reflect.ValueOf(new(imlAiAPIUseStore)) + }) } diff --git a/stores/api/model.go b/stores/api/model.go index 0c81e9db..d530cf3f 100644 --- a/stores/api/model.go +++ b/stores/api/model.go @@ -55,7 +55,7 @@ type Doc struct { Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` Service string `gorm:"size:36;not null;column:service;comment:服务;index:service"` - Content string `gorm:"type:text;null;column:content;comment:文档内容"` + Content string `gorm:"type:longtext;null;column:content;comment:文档内容"` Updater string `gorm:"size:36;not null;column:updater;comment:更新人;index:updater" aovalue:"updater"` UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:update_at;comment:更新时间"` APICount int64 `gorm:"type:int(11);not null;column:api_count;comment:接口数量"` @@ -79,11 +79,14 @@ type AiAPIInfo struct { Timeout int `gorm:"type:int(11);not null;column:timeout;comment:超时时间"` Retry int `gorm:"type:int(11);not null;column:retry;comment:重试次数"` Model string `gorm:"size:255;not null;column:model;comment:模型"` + Provider string `gorm:"size:36;not null;column:provider;comment:提供者;index:provider"` Creator string `gorm:"size:36;not null;column:creator;comment:创建人;index:creator" aovalue:"creator"` CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` Updater string `gorm:"size:36;not null;column:updater;comment:更新人;index:updater" aovalue:"updater"` UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:update_at;comment:更新时间"` AdditionalConfig string `gorm:"type:text;null;column:additional_config;comment:额外配置"` + UseToken int `gorm:"type:int(11);not null;column:use_token;comment:使用token"` + Disable bool `gorm:"type:tinyint(1);not null;column:disable;comment:是否禁用 0:否 1:是"` IsDelete bool `gorm:"type:tinyint(1);not null;column:is_delete;comment:是否删除 0:否 1:是"` } @@ -94,3 +97,25 @@ func (a *AiAPIInfo) TableName() string { func (a *AiAPIInfo) IdValue() int64 { return a.Id } + +type AiAPIUse struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + API string `gorm:"size:36;not null;column:api;comment:API;index:api"` + Service string `gorm:"size:36;not null;column:service;comment:服务;index:service"` + Provider string `gorm:"size:36;not null;column:provider;comment:提供者;index:provider"` + Model string `gorm:"size:255;not null;column:model;comment:模型"` + Day int64 `gorm:"type:int(11);not null;column:day;comment:当前日期"` + Hour int64 `gorm:"type:int(11);not null;column:hour;comment:当前小时"` + Minute int64 `gorm:"type:int(11);not null;column:minute;comment:当前分钟"` + InputToken int `gorm:"type:int(11);not null;column:input_token;comment:输入token"` + OutputToken int `gorm:"type:int(11);not null;column:output_token;comment:输出token"` + TotalToken int `gorm:"type:int(11);not null;column:total_token;comment:总token"` +} + +func (a *AiAPIUse) TableName() string { + return "ai_api_use" +} + +func (a *AiAPIUse) IdValue() int64 { + return a.Id +} diff --git a/stores/universally/commit/commit.go b/stores/universally/commit/commit.go index d6850960..700fb722 100644 --- a/stores/universally/commit/commit.go +++ b/stores/universally/commit/commit.go @@ -7,7 +7,7 @@ type Commit[H any] struct { UUID string `gorm:"size:36;not null;column:uuid;comment:uuid;uniqueIndex:uuid;"` Target string `gorm:"column:target;type:varchar(36);NOT NULL;comment:目标id;index:target;"` Key string `gorm:"size:50;not null;column:key;comment:类型;index:key;"` - Data *H `gorm:"type:text;not null;column:data;comment:数据;charset=utf8mb4;serializer:json"` + Data *H `gorm:"type:longtext;not null;column:data;comment:数据;charset=utf8mb4;serializer:json"` CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` Operator string `gorm:"size:36;not null;column:operator;comment:操作人;index:operator;"` }