Merge remote-tracking branch 'origin/main' into feature/1.4

This commit is contained in:
ningyv
2025-01-23 13:57:01 +08:00
91 changed files with 3059 additions and 572 deletions
+98
View File
@@ -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
+1 -1
View File
@@ -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
<br>
+7
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
/config.yml
+77
View File
@@ -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")
}
+167
View File
@@ -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
})
}
+34
View File
@@ -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
}
}
+7 -8
View File
@@ -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
+26
View File
@@ -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))
})
}
+69
View File
@@ -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)
}
+14 -2
View File
@@ -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{})
})
}
+72 -5
View File
@@ -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)
}
+2 -2
View File
@@ -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) {
+1 -1
View File
@@ -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 编辑
+3 -3
View File
@@ -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
@@ -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: (
<Button key="en" type="text" className="flex items-center p-0 bg-transparent border-none">
English
</Button>
),
title: 'English'
},
{
key: 'ja-JP',
label: (
<Button key="jp" type="text" className="flex items-center p-0 bg-transparent border-none">
</Button>
),
title: '日本語'
},
{
key: 'zh-TW',
label: (
<Button key="tw" type="text" className="flex items-center p-0 bg-transparent border-none">
</Button>
),
title: '繁體中文'
},
{
key: 'zh-CN',
label: (
<Button key="cn" type="text" className="flex items-center p-0 bg-transparent border-none">
</Button>
),
title: '简体中文'
}
]
const LanguageSetting = ({ mode = 'light' }: { mode?: 'dark' | 'light' }) => {
const { dispatch, state } = useGlobalContext()
const items = [
{
key: 'en-US',
label: (
<Button key="en" type="text" className="flex items-center p-0 bg-transparent border-none">
English
</Button>
),
title: 'English'
},
{
key: 'ja-JP',
label: (
<Button key="jp" type="text" className="flex items-center p-0 bg-transparent border-none">
</Button>
),
title: '日本語'
},
{
key: 'zh-TW',
label: (
<Button key="tw" type="text" className="flex items-center p-0 bg-transparent border-none">
</Button>
),
title: '繁體中文'
},
{
key: 'zh-CN',
label: (
<Button key="cn" type="text" className="flex items-center p-0 bg-transparent border-none">
</Button>
),
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' }) => {
<Dropdown
trigger={['hover']}
menu={{
items,
items: LanguageItems,
style: { minWidth: '80px' },
onClick: (e) => {
const { key } = e
@@ -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[]
@@ -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<Map<string, string[]>>(new Map())
@@ -510,7 +510,7 @@ export const useGlobalContext = () => {
updateDate: '',
powered: '',
mainPage: '',
language: 'en-US',
language: sessionStorage.getItem('i18nextLng') || 'en-US',
pluginsLoaded: false
},
dispatch: () => {},
+26 -6
View File
@@ -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
@@ -18,7 +18,11 @@ export const checkAccess: (access: AccessDataType, accessData: Map<string, strin
if (accLevel === 'team') {
accessSet = new Set(Array.from(accessSet).concat(accessData?.get('team') || []))
}
return accessSet!.size > 0 ? hasIntersection(neededBackendAccessArr, accessSet) : false
if (!accessSet!.size) {
return false
}
const hasAccess = hasIntersection(neededBackendAccessArr, accessSet)
return hasAccess
}
const hasIntersection = (arr1: string[], set1: Set<string>) => {
+6 -6
View File
@@ -151,15 +151,15 @@ function App() {
form={{ validateMessages }}
>
<PluginEventHubProvider>
<AppAntd className="h-full" message={{ maxCount: 1 }}>
<PluginSlotHubProvider>
<GlobalProvider>
<GlobalProvider>
<AppAntd className="h-full" message={{ maxCount: 1 }}>
<PluginSlotHubProvider>
<BreadcrumbProvider>
<RenderRoutes />
</BreadcrumbProvider>
</GlobalProvider>
</PluginSlotHubProvider>
</AppAntd>
</PluginSlotHubProvider>
</AppAntd>
</GlobalProvider>
</PluginEventHubProvider>
</ConfigProvider>
</StyleProvider>
@@ -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')
@@ -65,6 +65,11 @@ export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) =
{defaultLlm}
</div>
</div>
{status !== 'enabled' && alternativeModel && (
<div className="mt-1 text-sm text-gray-500">
{$t('关联 API 已转用')} {alternativeModel.name}/{alternativeModel.defaultLlm}
</div>
)}
</div>
{status !== 'enabled' && alternativeModel && (
<div className="ml-4 mt-1 text-sm text-gray-500">
@@ -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 (
<InsidePage
pageTitle={$t('常规设置')}
showBorder={false}
contentClassName="pr-PAGE_INSIDE_X"
scrollPage={false}
>
<Row className="mb-btnybase" >
<Col >
<span className="font-bold mr-[13px]">
{$t('API 请求设置')}
</span>
</Col>
</Row>
<ApiRequestSetting />
<Row className="mb-btnybase mt-[40px]">
<Col >
<span className="font-bold mr-[13px]">
{$t('服务分类')}
</span>
</Col>
</Row>
<ServiceCategory />
</InsidePage>
)
}
export default function CommonPage() {
const $t = useI18n()
return (
<InsidePage pageTitle={$t('常规设置')} showBorder={false} contentClassName="pr-PAGE_INSIDE_X" scrollPage={false}>
<Row className="mb-btnybase">
<Col>
<span className="font-bold mr-[13px]">{$t('API 请求设置')}</span>
</Col>
</Row>
<ApiRequestSetting />
<Row className="mb-btnybase mt-[40px]">
<Col>
<span className="font-bold mr-[13px]">{$t('服务分类')}</span>
</Col>
</Row>
<ServiceCategory />
</InsidePage>
)
}
@@ -28,7 +28,7 @@ export default function ServiceCategory() {
const { accessData } = useGlobalContext()
const [loading, setLoading] = useState<boolean>(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: (
<TreeWithMore stopClick={false} dropdownMenu={dropdownMenu(item as CategorizesType)}>
@@ -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 = (
<ServiceHubCategoryConfig WithPermission={WithPermission} ref={addRef} type={type} />
)
content = <ServiceHubCategoryConfig ref={addRef} type={type} />
break
}
case 'addChildCate':
title = $t('添加子分类')
content = (
<ServiceHubCategoryConfig
WithPermission={WithPermission}
ref={addChildRef}
type={type}
entity={entity}
/>
)
content = <ServiceHubCategoryConfig ref={addChildRef} type={type} entity={entity} />
break
case 'renameCate':
title = $t('重命名分类')
content = (
<ServiceHubCategoryConfig
WithPermission={WithPermission}
ref={renameRef}
type={type}
entity={entity}
/>
)
content = <ServiceHubCategoryConfig ref={renameRef} type={type} entity={entity} />
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<BasicResponse<null>>('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<BasicResponse<{ catalogues: CategorizesType[]; tags: EntityItem[] }>>('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 (
<div className="border border-solid border-BORDER p-[20px] rounded-[10px] ">
<Spin
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
spinning={loading}
className=""
>
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} className="">
<Tree
showIcon
draggable
@@ -10,132 +10,131 @@ import {
import { App, Form, Input } from 'antd'
import { forwardRef, useEffect, useImperativeHandle } from 'react'
export const ServiceHubCategoryConfig = forwardRef<
ServiceHubCategoryConfigHandle,
ServiceHubCategoryConfigProps
>((props, ref) => {
const { message } = App.useApp()
const [form] = Form.useForm()
const { type, entity } = props
const { fetchData } = useFetch()
export const ServiceHubCategoryConfig = forwardRef<ServiceHubCategoryConfigHandle, ServiceHubCategoryConfigProps>(
(props, ref) => {
const { message } = App.useApp()
const [form] = Form.useForm()
const { type, entity } = props
const { fetchData } = useFetch()
const save: () => Promise<boolean | string> = () => {
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<boolean | string> = () => {
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<BasicResponse<null>>(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<BasicResponse<null>>(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 (
<WithPermission
access={
type === 'addCate'
? 'system.api_market.service_classification.add'
: 'system.api_market.service_classification.edit'
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
}
>
<Form
layout="vertical"
scrollToFirstError
labelAlign="left"
form={form}
className="mx-auto"
name="serviceHubCategoryConfig"
autoComplete="off"
>
{type === 'renameCate' && (
<Form.Item<ServiceHubCategoryConfigFieldType>
label={$t('ID')}
name="id"
hidden
rules={[{ required: true, whitespace: true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
)}
{(type === 'addCate' || type === 'renameCate') && (
<Form.Item<ServiceHubCategoryConfigFieldType>
label={$t('分类名称')}
name="name"
rules={[{ required: true, whitespace: true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
)}
}, [])
{type === 'addChildCate' && (
<>
return (
<WithPermission
access={
type === 'addCate'
? 'system.api_market.service_classification.add'
: 'system.api_market.service_classification.edit'
}
>
<Form
layout="vertical"
scrollToFirstError
labelAlign="left"
form={form}
className="mx-auto"
name="serviceHubCategoryConfig"
autoComplete="off"
>
{type === 'renameCate' && (
<Form.Item<ServiceHubCategoryConfigFieldType>
label={$t('父分类 ID')}
name="parent"
label={$t('ID')}
name="id"
hidden
rules={[{ required: true, whitespace: true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
)}
{(type === 'addCate' || type === 'renameCate') && (
<Form.Item<ServiceHubCategoryConfigFieldType>
label={$t('分类名称')}
label={$t('分类名称')}
name="name"
rules={[{ required: true, whitespace: true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
</>
)}
</Form>
</WithPermission>
)
})
)}
{type === 'addChildCate' && (
<>
<Form.Item<ServiceHubCategoryConfigFieldType>
label={$t('父分类 ID')}
name="parent"
hidden
rules={[{ required: true, whitespace: true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
<Form.Item<ServiceHubCategoryConfigFieldType>
label={$t('子分类名称')}
name="name"
rules={[{ required: true, whitespace: true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
</>
)}
</Form>
</WithPermission>
)
}
)
@@ -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 = {
@@ -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 = (
<GlobalProvider>
<ManagementConfig ref={addManagementRef} dataShowType={dataShowType} type={type} teamId={teamId!} />
</GlobalProvider>
)
content = <ManagementConfig ref={addManagementRef} dataShowType={dataShowType} type={type} teamId={teamId!} />
break
// case 'edit':{
// title='配置 Open Api'
// message.loading('正在加载数据')
// const {code,data,msg} = await fetchData<BasicResponse<{app:ManagementConfigFieldType}>>('external-app',{method:'GET',eoParams:{id:entity!.id}})
// message.destroy()
// if(code === STATUS_CODE.SUCCESS){
// content=<ManagementConfig ref={editManagementRef} type={type} entity={data.app}/>
// }else{
// message.error(msg || $t(RESPONSE_TIPS.error))
// return
// }
// break;}
// case 'delete':
// title='删除'
// content='该数据删除后将无法找回,请确认是否删除?'
// break;
}
modal.confirm({
+5 -2
View File
@@ -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
+1 -1
View File
@@ -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"
+9
View File
@@ -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 {
+5 -3
View File
@@ -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
+7 -2
View File
@@ -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=
+1
View File
@@ -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"
+2 -2
View File
@@ -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"`
+2 -2
View File
@@ -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"`
+32
View File
@@ -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
}
}
}
}
+2 -1
View File
@@ -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 {
+49
View File
@@ -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
}
}
+20
View File
@@ -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"`
}
+21
View File
@@ -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"`
}
+447
View File
@@ -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)
})
}
+25
View File
@@ -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))
})
}
+39
View File
@@ -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
}
}
+6
View File
@@ -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"`
}
+81 -20
View File
@@ -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"`
}
+536 -155
View File
@@ -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
}
}
+17 -4
View File
@@ -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))
})
}
-13
View File
@@ -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 {
+2 -2
View File
@@ -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 {
+5 -1
View File
@@ -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 {
+2 -2
View File
@@ -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"`
}
+2 -2
View File
@@ -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"`
}
-23
View File
@@ -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
+32 -19
View File
@@ -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 {
+20 -4
View File
@@ -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),
}
}
+5
View File
@@ -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()...)
}
+18
View File
@@ -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),
}
}
+45
View File
@@ -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
}
}
+19
View File
@@ -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
}
+33
View File
@@ -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()
}
+1 -1
View File
@@ -211,7 +211,7 @@ APIParkはApache 2.0ライセンスの下で提供されています。詳細に
エンタープライズ機能や専門的な技術サポートについては、プリセールスの専門家に連絡し、個別デモ、カスタムソリューション、価格情報を入手してください。
- ウェブサイト: https://apipark.com
- メール: dev@apipark.com
- メール: contact@apipark.com
<br>
+1 -1
View File
@@ -215,7 +215,7 @@ APIPark 使用 Apache 2.0 许可证。更多详情请查看 LICENSE 文件。
对于企业级功能和专业技术支持,请联系售前专家进行个性化演示、定制方案和获取报价。
- 网站: https://apipark.com
- 电子邮件: dev@apipark.com
- 电子邮件: contact@apipark.com
<br>
+1 -1
View File
@@ -212,7 +212,7 @@ APIPark 使用 Apache 2.0 授權條款。更多詳情請參閱 LICENSE 文件。
如需企業級功能與專業技術支援,請聯絡我們的售前專家,獲取個性化演示、定制方案和報價。
- 網站: https://apipark.com
- 電子郵件: dev@apipark.com
- 電子郵件: contact@apipark.com
<br>
+30
View File
@@ -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'
+12
View File
@@ -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
+2 -2
View File
@@ -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"
+3
View File
@@ -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}/
+9
View File
@@ -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}"
+1 -1
View File
@@ -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)
+6 -1
View File
@@ -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
nohup ./apipark >> run.log 2>&1 &
nohup ./apipark_ai_event_listen >> run.log 2>&1 &
tail -F run.log
+2 -1
View File
@@ -42,7 +42,8 @@ start() {
cat "$LOG_FILE"
exit 1
fi
# 启动ai事件监听程序
# nohup ./apipark_ai_event_listen >> "$LOG_FILE" 2>&1 &
}
# 停止函数
+93 -2
View File
@@ -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
}
+29
View File
@@ -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
}
+12 -2
View File
@@ -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))
})
}
+244
View File
@@ -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()
}
+61
View File
@@ -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
}
+31
View File
@@ -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))
})
}
+25 -2
View File
@@ -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
}
+7 -3
View File
@@ -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,
}
}
+3 -1
View File
@@ -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() {
+1 -1
View File
@@ -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 {
+3
View File
@@ -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 (
+3 -2
View File
@@ -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
}
+30
View File
@@ -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"
}
+22
View File
@@ -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))
})
}
+47 -1
View File
@@ -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
}
+26 -1
View File
@@ -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))
})
}
+12
View File
@@ -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))
})
}
+26 -1
View File
@@ -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
}
+1 -1
View File
@@ -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;"`
}