diff --git a/backend/api/ai.api b/backend/api/ai.api index fc8d645..16f16c9 100644 --- a/backend/api/ai.api +++ b/backend/api/ai.api @@ -99,6 +99,56 @@ type ( } ) +// ========== Provider Types ========== + +type ( + AIProviderInfo { + Id int64 `json:"id,string"` + Name string `json:"name"` + DisplayName string `json:"displayName"` + BaseUrl string `json:"baseUrl"` + SdkType string `json:"sdkType"` + Protocol string `json:"protocol"` + IsActive bool `json:"isActive"` + SortOrder int `json:"sortOrder"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } + + AIProviderListRequest { + Page int64 `form:"page,optional,default=1"` + PageSize int64 `form:"pageSize,optional,default=50"` + } + + AIProviderListResponse { + List []AIProviderInfo `json:"list"` + Total int64 `json:"total"` + } + + AIProviderCreateRequest { + Name string `json:"name"` + DisplayName string `json:"displayName"` + BaseUrl string `json:"baseUrl"` + SdkType string `json:"sdkType"` + Protocol string `json:"protocol,optional,default=openai"` + SortOrder int `json:"sortOrder,optional"` + } + + AIProviderUpdateRequest { + Id int64 `path:"id"` + DisplayName string `json:"displayName,optional"` + BaseUrl string `json:"baseUrl,optional"` + SdkType string `json:"sdkType,optional"` + Protocol string `json:"protocol,optional"` + IsActive bool `json:"isActive,optional"` + SortOrder int `json:"sortOrder,optional"` + } + + AIProviderDeleteRequest { + Id int64 `path:"id"` + } +) + // ========== Model Types ========== type ( @@ -119,6 +169,76 @@ type ( AIModelListResponse { List []AIModelInfo `json:"list"` } + + AIModelCreateRequest { + ProviderId int64 `json:"providerId,string"` + ModelId string `json:"modelId"` + DisplayName string `json:"displayName"` + InputPrice float64 `json:"inputPrice"` + OutputPrice float64 `json:"outputPrice"` + MaxTokens int `json:"maxTokens,optional,default=4096"` + ContextWindow int `json:"contextWindow,optional,default=128000"` + SupportsStream bool `json:"supportsStream,optional"` + SupportsVision bool `json:"supportsVision,optional"` + } + + AIModelUpdateRequest { + Id int64 `path:"id"` + DisplayName string `json:"displayName,optional"` + InputPrice float64 `json:"inputPrice,optional"` + OutputPrice float64 `json:"outputPrice,optional"` + MaxTokens int `json:"maxTokens,optional"` + ContextWindow int `json:"contextWindow,optional"` + SupportsStream bool `json:"supportsStream,optional"` + SupportsVision bool `json:"supportsVision,optional"` + IsActive bool `json:"isActive,optional"` + } + + AIModelDeleteRequest { + Id int64 `path:"id"` + } +) + +// ========== API Key Types ========== + +type ( + AIApiKeyInfo { + Id int64 `json:"id,string"` + ProviderId int64 `json:"providerId,string"` + ProviderName string `json:"providerName"` + UserId int64 `json:"userId,string"` + KeyPreview string `json:"keyPreview"` + IsActive bool `json:"isActive"` + Remark string `json:"remark"` + CreatedAt string `json:"createdAt"` + } + + AIApiKeyListRequest { + Page int64 `form:"page,optional,default=1"` + PageSize int64 `form:"pageSize,optional,default=20"` + } + + AIApiKeyListResponse { + List []AIApiKeyInfo `json:"list"` + Total int64 `json:"total"` + } + + AIApiKeyCreateRequest { + ProviderId int64 `json:"providerId,string"` + KeyValue string `json:"keyValue"` + Remark string `json:"remark,optional"` + } + + AIApiKeyUpdateRequest { + Id int64 `path:"id"` + KeyValue string `json:"keyValue,optional"` + IsActive bool `json:"isActive,optional"` + Remark string `json:"remark,optional"` + } + + AIApiKeyDeleteRequest { + Id int64 `path:"id"` + } ) // ========== Quota Types ========== diff --git a/backend/base.api b/backend/base.api index 8a74033..4930168 100644 --- a/backend/base.api +++ b/backend/base.api @@ -333,5 +333,57 @@ service base-api { @doc "获取我的配额" @handler AiQuotaMe get /ai/quota/me returns (AIQuotaInfo) + + @doc "获取我的API Key列表" + @handler AiApiKeyList + get /ai/keys (AIApiKeyListRequest) returns (AIApiKeyListResponse) + + @doc "添加API Key" + @handler AiApiKeyCreate + post /ai/key (AIApiKeyCreateRequest) returns (AIApiKeyInfo) + + @doc "更新API Key" + @handler AiApiKeyUpdate + put /ai/key/:id (AIApiKeyUpdateRequest) returns (AIApiKeyInfo) + + @doc "删除API Key" + @handler AiApiKeyDelete + delete /ai/key/:id (AIApiKeyDeleteRequest) returns (Response) +} + +// ========== AI 管理(admin 权限)========== +@server ( + prefix: /api/v1 + group: ai + middleware: Cors,Log,Auth,Authz +) +service base-api { + @doc "获取AI平台列表" + @handler AiProviderList + get /ai/providers (AIProviderListRequest) returns (AIProviderListResponse) + + @doc "创建AI平台" + @handler AiProviderCreate + post /ai/provider (AIProviderCreateRequest) returns (AIProviderInfo) + + @doc "更新AI平台" + @handler AiProviderUpdate + put /ai/provider/:id (AIProviderUpdateRequest) returns (AIProviderInfo) + + @doc "删除AI平台" + @handler AiProviderDelete + delete /ai/provider/:id (AIProviderDeleteRequest) returns (Response) + + @doc "创建AI模型" + @handler AiModelCreate + post /ai/model (AIModelCreateRequest) returns (AIModelInfo) + + @doc "更新AI模型" + @handler AiModelUpdate + put /ai/model/:id (AIModelUpdateRequest) returns (AIModelInfo) + + @doc "删除AI模型" + @handler AiModelDelete + delete /ai/model/:id (AIModelDeleteRequest) returns (Response) } diff --git a/backend/internal/handler/ai/aiapikeycreatehandler.go b/backend/internal/handler/ai/aiapikeycreatehandler.go new file mode 100644 index 0000000..3da636b --- /dev/null +++ b/backend/internal/handler/ai/aiapikeycreatehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/ai" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 添加API Key +func AiApiKeyCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AIApiKeyCreateRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ai.NewAiApiKeyCreateLogic(r.Context(), svcCtx) + resp, err := l.AiApiKeyCreate(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/ai/aiapikeydeletehandler.go b/backend/internal/handler/ai/aiapikeydeletehandler.go new file mode 100644 index 0000000..39567c5 --- /dev/null +++ b/backend/internal/handler/ai/aiapikeydeletehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/ai" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 删除API Key +func AiApiKeyDeleteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AIApiKeyDeleteRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ai.NewAiApiKeyDeleteLogic(r.Context(), svcCtx) + resp, err := l.AiApiKeyDelete(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/ai/aiapikeylisthandler.go b/backend/internal/handler/ai/aiapikeylisthandler.go new file mode 100644 index 0000000..e0b873e --- /dev/null +++ b/backend/internal/handler/ai/aiapikeylisthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/ai" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 获取我的API Key列表 +func AiApiKeyListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AIApiKeyListRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ai.NewAiApiKeyListLogic(r.Context(), svcCtx) + resp, err := l.AiApiKeyList(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/ai/aiapikeyupdatehandler.go b/backend/internal/handler/ai/aiapikeyupdatehandler.go new file mode 100644 index 0000000..1ece175 --- /dev/null +++ b/backend/internal/handler/ai/aiapikeyupdatehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/ai" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 更新API Key +func AiApiKeyUpdateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AIApiKeyUpdateRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ai.NewAiApiKeyUpdateLogic(r.Context(), svcCtx) + resp, err := l.AiApiKeyUpdate(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/ai/aimodelcreatehandler.go b/backend/internal/handler/ai/aimodelcreatehandler.go new file mode 100644 index 0000000..d368498 --- /dev/null +++ b/backend/internal/handler/ai/aimodelcreatehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/ai" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 创建AI模型 +func AiModelCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AIModelCreateRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ai.NewAiModelCreateLogic(r.Context(), svcCtx) + resp, err := l.AiModelCreate(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/ai/aimodeldeletehandler.go b/backend/internal/handler/ai/aimodeldeletehandler.go new file mode 100644 index 0000000..8e97482 --- /dev/null +++ b/backend/internal/handler/ai/aimodeldeletehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/ai" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 删除AI模型 +func AiModelDeleteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AIModelDeleteRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ai.NewAiModelDeleteLogic(r.Context(), svcCtx) + resp, err := l.AiModelDelete(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/ai/aimodelupdatehandler.go b/backend/internal/handler/ai/aimodelupdatehandler.go new file mode 100644 index 0000000..f7a512e --- /dev/null +++ b/backend/internal/handler/ai/aimodelupdatehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/ai" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 更新AI模型 +func AiModelUpdateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AIModelUpdateRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ai.NewAiModelUpdateLogic(r.Context(), svcCtx) + resp, err := l.AiModelUpdate(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/ai/aiprovidercreatehandler.go b/backend/internal/handler/ai/aiprovidercreatehandler.go new file mode 100644 index 0000000..f714323 --- /dev/null +++ b/backend/internal/handler/ai/aiprovidercreatehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/ai" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 创建AI平台 +func AiProviderCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AIProviderCreateRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ai.NewAiProviderCreateLogic(r.Context(), svcCtx) + resp, err := l.AiProviderCreate(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/ai/aiproviderdeletehandler.go b/backend/internal/handler/ai/aiproviderdeletehandler.go new file mode 100644 index 0000000..597a79f --- /dev/null +++ b/backend/internal/handler/ai/aiproviderdeletehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/ai" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 删除AI平台 +func AiProviderDeleteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AIProviderDeleteRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ai.NewAiProviderDeleteLogic(r.Context(), svcCtx) + resp, err := l.AiProviderDelete(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/ai/aiproviderlisthandler.go b/backend/internal/handler/ai/aiproviderlisthandler.go new file mode 100644 index 0000000..4afc8a1 --- /dev/null +++ b/backend/internal/handler/ai/aiproviderlisthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/ai" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 获取AI平台列表 +func AiProviderListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AIProviderListRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ai.NewAiProviderListLogic(r.Context(), svcCtx) + resp, err := l.AiProviderList(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/ai/aiproviderupdatehandler.go b/backend/internal/handler/ai/aiproviderupdatehandler.go new file mode 100644 index 0000000..b578d04 --- /dev/null +++ b/backend/internal/handler/ai/aiproviderupdatehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/ai" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 更新AI平台 +func AiProviderUpdateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AIProviderUpdateRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ai.NewAiProviderUpdateLogic(r.Context(), svcCtx) + resp, err := l.AiProviderUpdate(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/routes.go b/backend/internal/handler/routes.go index 1bc662d..4c9c73b 100644 --- a/backend/internal/handler/routes.go +++ b/backend/internal/handler/routes.go @@ -61,6 +61,30 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/ai/conversations", Handler: ai.AiConversationListHandler(serverCtx), }, + { + // 添加API Key + Method: http.MethodPost, + Path: "/ai/key", + Handler: ai.AiApiKeyCreateHandler(serverCtx), + }, + { + // 更新API Key + Method: http.MethodPut, + Path: "/ai/key/:id", + Handler: ai.AiApiKeyUpdateHandler(serverCtx), + }, + { + // 删除API Key + Method: http.MethodDelete, + Path: "/ai/key/:id", + Handler: ai.AiApiKeyDeleteHandler(serverCtx), + }, + { + // 获取我的API Key列表 + Method: http.MethodGet, + Path: "/ai/keys", + Handler: ai.AiApiKeyListHandler(serverCtx), + }, { // 获取模型列表 Method: http.MethodGet, @@ -78,6 +102,57 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { rest.WithPrefix("/api/v1"), ) + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.Cors, serverCtx.Log, serverCtx.Auth, serverCtx.Authz}, + []rest.Route{ + { + // 创建AI模型 + Method: http.MethodPost, + Path: "/ai/model", + Handler: ai.AiModelCreateHandler(serverCtx), + }, + { + // 更新AI模型 + Method: http.MethodPut, + Path: "/ai/model/:id", + Handler: ai.AiModelUpdateHandler(serverCtx), + }, + { + // 删除AI模型 + Method: http.MethodDelete, + Path: "/ai/model/:id", + Handler: ai.AiModelDeleteHandler(serverCtx), + }, + { + // 创建AI平台 + Method: http.MethodPost, + Path: "/ai/provider", + Handler: ai.AiProviderCreateHandler(serverCtx), + }, + { + // 更新AI平台 + Method: http.MethodPut, + Path: "/ai/provider/:id", + Handler: ai.AiProviderUpdateHandler(serverCtx), + }, + { + // 删除AI平台 + Method: http.MethodDelete, + Path: "/ai/provider/:id", + Handler: ai.AiProviderDeleteHandler(serverCtx), + }, + { + // 获取AI平台列表 + Method: http.MethodGet, + Path: "/ai/providers", + Handler: ai.AiProviderListHandler(serverCtx), + }, + }..., + ), + rest.WithPrefix("/api/v1"), + ) + server.AddRoutes( rest.WithMiddlewares( []rest.Middleware{serverCtx.Cors, serverCtx.Log}, diff --git a/backend/internal/logic/ai/aiapikeycreatelogic.go b/backend/internal/logic/ai/aiapikeycreatelogic.go new file mode 100644 index 0000000..447f316 --- /dev/null +++ b/backend/internal/logic/ai/aiapikeycreatelogic.go @@ -0,0 +1,73 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "context" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AiApiKeyCreateLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 创建API Key +func NewAiApiKeyCreateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiApiKeyCreateLogic { + return &AiApiKeyCreateLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AiApiKeyCreateLogic) AiApiKeyCreate(req *types.AIApiKeyCreateRequest) (resp *types.AIApiKeyInfo, err error) { + userId, _ := l.ctx.Value("userId").(int64) + + apiKey := &model.AIApiKey{ + ProviderId: req.ProviderId, + UserId: userId, + KeyValue: req.KeyValue, + IsActive: true, + Remark: req.Remark, + } + + id, err := model.AIApiKeyInsert(l.ctx, l.svcCtx.DB, apiKey) + if err != nil { + return nil, fmt.Errorf("创建API Key失败: %v", err) + } + + // Retrieve the inserted record to get timestamps + created, err := model.AIApiKeyFindOne(l.ctx, l.svcCtx.DB, id) + if err != nil { + return nil, fmt.Errorf("查询API Key失败: %v", err) + } + + // Look up provider name + providerName := "" + provider, provErr := model.AIProviderFindOne(l.ctx, l.svcCtx.DB, created.ProviderId) + if provErr == nil { + providerName = provider.DisplayName + } + + resp = &types.AIApiKeyInfo{ + Id: created.Id, + ProviderId: created.ProviderId, + ProviderName: providerName, + UserId: created.UserId, + KeyPreview: maskKey(created.KeyValue), + IsActive: created.IsActive, + Remark: created.Remark, + CreatedAt: created.CreatedAt.Format("2006-01-02 15:04:05"), + } + + return resp, nil +} diff --git a/backend/internal/logic/ai/aiapikeydeletelogic.go b/backend/internal/logic/ai/aiapikeydeletelogic.go new file mode 100644 index 0000000..1e52eaf --- /dev/null +++ b/backend/internal/logic/ai/aiapikeydeletelogic.go @@ -0,0 +1,60 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "context" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AiApiKeyDeleteLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 删除API Key +func NewAiApiKeyDeleteLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiApiKeyDeleteLogic { + return &AiApiKeyDeleteLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AiApiKeyDeleteLogic) AiApiKeyDelete(req *types.AIApiKeyDeleteRequest) (resp *types.Response, err error) { + userId, _ := l.ctx.Value("userId").(int64) + + // Find existing key + existing, err := model.AIApiKeyFindOne(l.ctx, l.svcCtx.DB, req.Id) + if err != nil { + if err == model.ErrNotFound { + return nil, fmt.Errorf("API Key不存在") + } + return nil, fmt.Errorf("查询API Key失败: %v", err) + } + + // Verify ownership + if existing.UserId != userId && existing.UserId != 0 { + return nil, fmt.Errorf("无权操作此API Key") + } + + if err = model.AIApiKeyDelete(l.ctx, l.svcCtx.DB, req.Id); err != nil { + return nil, fmt.Errorf("删除API Key失败: %v", err) + } + + resp = &types.Response{ + Code: 0, + Message: "success", + Success: true, + } + + return resp, nil +} diff --git a/backend/internal/logic/ai/aiapikeylistlogic.go b/backend/internal/logic/ai/aiapikeylistlogic.go new file mode 100644 index 0000000..e6afd2d --- /dev/null +++ b/backend/internal/logic/ai/aiapikeylistlogic.go @@ -0,0 +1,90 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "context" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AiApiKeyListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 获取我的API Key列表 +func NewAiApiKeyListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiApiKeyListLogic { + return &AiApiKeyListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AiApiKeyListLogic) AiApiKeyList(req *types.AIApiKeyListRequest) (resp *types.AIApiKeyListResponse, err error) { + userId, _ := l.ctx.Value("userId").(int64) + + // Query keys belonging to current user or system keys (userId=0) + var keys []model.AIApiKey + var total int64 + query := l.svcCtx.DB.WithContext(l.ctx).Model(&model.AIApiKey{}).Where("user_id = ? OR user_id = 0", userId) + if err = query.Count(&total).Error; err != nil { + return nil, err + } + + offset := (req.Page - 1) * req.PageSize + if offset < 0 { + offset = 0 + } + if err = query.Order("created_at DESC").Offset(int(offset)).Limit(int(req.PageSize)).Find(&keys).Error; err != nil { + return nil, err + } + + // Build provider name cache to avoid repeated queries + providerCache := make(map[int64]string) + list := make([]types.AIApiKeyInfo, 0, len(keys)) + for _, key := range keys { + providerName := "" + if name, ok := providerCache[key.ProviderId]; ok { + providerName = name + } else { + provider, provErr := model.AIProviderFindOne(l.ctx, l.svcCtx.DB, key.ProviderId) + if provErr == nil { + providerName = provider.DisplayName + } + providerCache[key.ProviderId] = providerName + } + + list = append(list, types.AIApiKeyInfo{ + Id: key.Id, + ProviderId: key.ProviderId, + ProviderName: providerName, + UserId: key.UserId, + KeyPreview: maskKey(key.KeyValue), + IsActive: key.IsActive, + Remark: key.Remark, + CreatedAt: key.CreatedAt.Format("2006-01-02 15:04:05"), + }) + } + + resp = &types.AIApiKeyListResponse{ + List: list, + Total: total, + } + + return resp, nil +} + +func maskKey(key string) string { + if len(key) <= 10 { + return "sk-***" + } + return key[:6] + "..." + key[len(key)-4:] +} diff --git a/backend/internal/logic/ai/aiapikeyupdatelogic.go b/backend/internal/logic/ai/aiapikeyupdatelogic.go new file mode 100644 index 0000000..b063111 --- /dev/null +++ b/backend/internal/logic/ai/aiapikeyupdatelogic.go @@ -0,0 +1,79 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "context" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AiApiKeyUpdateLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 更新API Key +func NewAiApiKeyUpdateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiApiKeyUpdateLogic { + return &AiApiKeyUpdateLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AiApiKeyUpdateLogic) AiApiKeyUpdate(req *types.AIApiKeyUpdateRequest) (resp *types.AIApiKeyInfo, err error) { + userId, _ := l.ctx.Value("userId").(int64) + + // Find existing key + existing, err := model.AIApiKeyFindOne(l.ctx, l.svcCtx.DB, req.Id) + if err != nil { + if err == model.ErrNotFound { + return nil, fmt.Errorf("API Key不存在") + } + return nil, fmt.Errorf("查询API Key失败: %v", err) + } + + // Verify ownership: must be the user's own key or a system key (userId=0) + if existing.UserId != userId && existing.UserId != 0 { + return nil, fmt.Errorf("无权更新此API Key") + } + + // Update fields + if req.KeyValue != "" { + existing.KeyValue = req.KeyValue + } + existing.IsActive = req.IsActive + existing.Remark = req.Remark + + if err = model.AIApiKeyUpdate(l.ctx, l.svcCtx.DB, existing); err != nil { + return nil, fmt.Errorf("更新API Key失败: %v", err) + } + + // Look up provider name + providerName := "" + provider, provErr := model.AIProviderFindOne(l.ctx, l.svcCtx.DB, existing.ProviderId) + if provErr == nil { + providerName = provider.DisplayName + } + + resp = &types.AIApiKeyInfo{ + Id: existing.Id, + ProviderId: existing.ProviderId, + ProviderName: providerName, + UserId: existing.UserId, + KeyPreview: maskKey(existing.KeyValue), + IsActive: existing.IsActive, + Remark: existing.Remark, + CreatedAt: existing.CreatedAt.Format("2006-01-02 15:04:05"), + } + + return resp, nil +} diff --git a/backend/internal/logic/ai/aimodelcreatelogic.go b/backend/internal/logic/ai/aimodelcreatelogic.go new file mode 100644 index 0000000..383ea41 --- /dev/null +++ b/backend/internal/logic/ai/aimodelcreatelogic.go @@ -0,0 +1,79 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "context" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AiModelCreateLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 创建AI模型 +func NewAiModelCreateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiModelCreateLogic { + return &AiModelCreateLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AiModelCreateLogic) AiModelCreate(req *types.AIModelCreateRequest) (resp *types.AIModelInfo, err error) { + aiModel := &model.AIModel{ + ProviderId: req.ProviderId, + ModelId: req.ModelId, + DisplayName: req.DisplayName, + InputPrice: req.InputPrice, + OutputPrice: req.OutputPrice, + MaxTokens: req.MaxTokens, + ContextWindow: req.ContextWindow, + SupportsStream: req.SupportsStream, + SupportsVision: req.SupportsVision, + IsActive: true, + } + + id, err := model.AIModelInsert(l.ctx, l.svcCtx.DB, aiModel) + if err != nil { + return nil, fmt.Errorf("创建AI模型失败: %v", err) + } + + // 查询刚创建的记录以获取完整字段 + aiModel, err = model.AIModelFindOne(l.ctx, l.svcCtx.DB, id) + if err != nil { + return nil, fmt.Errorf("查询AI模型失败: %v", err) + } + + // 查询供应商名称 + providerName := "" + provider, err := model.AIProviderFindOne(l.ctx, l.svcCtx.DB, aiModel.ProviderId) + if err == nil { + providerName = provider.DisplayName + } + + resp = &types.AIModelInfo{ + Id: aiModel.Id, + ProviderId: aiModel.ProviderId, + ProviderName: providerName, + ModelId: aiModel.ModelId, + DisplayName: aiModel.DisplayName, + InputPrice: aiModel.InputPrice, + OutputPrice: aiModel.OutputPrice, + MaxTokens: aiModel.MaxTokens, + ContextWindow: aiModel.ContextWindow, + SupportsStream: aiModel.SupportsStream, + SupportsVision: aiModel.SupportsVision, + } + + return resp, nil +} diff --git a/backend/internal/logic/ai/aimodeldeletelogic.go b/backend/internal/logic/ai/aimodeldeletelogic.go new file mode 100644 index 0000000..19def51 --- /dev/null +++ b/backend/internal/logic/ai/aimodeldeletelogic.go @@ -0,0 +1,44 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "context" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AiModelDeleteLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 删除AI模型 +func NewAiModelDeleteLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiModelDeleteLogic { + return &AiModelDeleteLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AiModelDeleteLogic) AiModelDelete(req *types.AIModelDeleteRequest) (resp *types.Response, err error) { + err = model.AIModelDelete(l.ctx, l.svcCtx.DB, req.Id) + if err != nil { + return nil, err + } + + resp = &types.Response{ + Code: 0, + Message: "success", + Success: true, + } + + return resp, nil +} diff --git a/backend/internal/logic/ai/aimodelupdatelogic.go b/backend/internal/logic/ai/aimodelupdatelogic.go new file mode 100644 index 0000000..81ee0f5 --- /dev/null +++ b/backend/internal/logic/ai/aimodelupdatelogic.go @@ -0,0 +1,76 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "context" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AiModelUpdateLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 更新AI模型 +func NewAiModelUpdateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiModelUpdateLogic { + return &AiModelUpdateLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AiModelUpdateLogic) AiModelUpdate(req *types.AIModelUpdateRequest) (resp *types.AIModelInfo, err error) { + // 查询现有记录 + aiModel, err := model.AIModelFindOne(l.ctx, l.svcCtx.DB, req.Id) + if err != nil { + return nil, fmt.Errorf("查询AI模型失败: %v", err) + } + + // 更新字段 + aiModel.DisplayName = req.DisplayName + aiModel.InputPrice = req.InputPrice + aiModel.OutputPrice = req.OutputPrice + aiModel.MaxTokens = req.MaxTokens + aiModel.ContextWindow = req.ContextWindow + aiModel.SupportsStream = req.SupportsStream + aiModel.SupportsVision = req.SupportsVision + aiModel.IsActive = req.IsActive + + err = model.AIModelUpdate(l.ctx, l.svcCtx.DB, aiModel) + if err != nil { + return nil, fmt.Errorf("更新AI模型失败: %v", err) + } + + // 查询供应商名称 + providerName := "" + provider, err := model.AIProviderFindOne(l.ctx, l.svcCtx.DB, aiModel.ProviderId) + if err == nil { + providerName = provider.DisplayName + } + + resp = &types.AIModelInfo{ + Id: aiModel.Id, + ProviderId: aiModel.ProviderId, + ProviderName: providerName, + ModelId: aiModel.ModelId, + DisplayName: aiModel.DisplayName, + InputPrice: aiModel.InputPrice, + OutputPrice: aiModel.OutputPrice, + MaxTokens: aiModel.MaxTokens, + ContextWindow: aiModel.ContextWindow, + SupportsStream: aiModel.SupportsStream, + SupportsVision: aiModel.SupportsVision, + } + + return resp, nil +} diff --git a/backend/internal/logic/ai/aiprovidercreatelogic.go b/backend/internal/logic/ai/aiprovidercreatelogic.go new file mode 100644 index 0000000..223efb8 --- /dev/null +++ b/backend/internal/logic/ai/aiprovidercreatelogic.go @@ -0,0 +1,60 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "context" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AiProviderCreateLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 创建AI平台 +func NewAiProviderCreateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiProviderCreateLogic { + return &AiProviderCreateLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AiProviderCreateLogic) AiProviderCreate(req *types.AIProviderCreateRequest) (resp *types.AIProviderInfo, err error) { + provider := &model.AIProvider{ + Name: req.Name, + DisplayName: req.DisplayName, + BaseUrl: req.BaseUrl, + SdkType: req.SdkType, + Protocol: req.Protocol, + IsActive: true, + SortOrder: req.SortOrder, + } + + _, err = model.AIProviderInsert(l.ctx, l.svcCtx.DB, provider) + if err != nil { + return nil, fmt.Errorf("创建AI平台失败: %v", err) + } + + return &types.AIProviderInfo{ + Id: provider.Id, + Name: provider.Name, + DisplayName: provider.DisplayName, + BaseUrl: provider.BaseUrl, + SdkType: provider.SdkType, + Protocol: provider.Protocol, + IsActive: provider.IsActive, + SortOrder: provider.SortOrder, + CreatedAt: provider.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: provider.UpdatedAt.Format("2006-01-02 15:04:05"), + }, nil +} diff --git a/backend/internal/logic/ai/aiproviderdeletelogic.go b/backend/internal/logic/ai/aiproviderdeletelogic.go new file mode 100644 index 0000000..bb7771e --- /dev/null +++ b/backend/internal/logic/ai/aiproviderdeletelogic.go @@ -0,0 +1,42 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "context" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AiProviderDeleteLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 删除AI平台 +func NewAiProviderDeleteLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiProviderDeleteLogic { + return &AiProviderDeleteLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AiProviderDeleteLogic) AiProviderDelete(req *types.AIProviderDeleteRequest) (resp *types.Response, err error) { + if err := model.AIProviderDelete(l.ctx, l.svcCtx.DB, req.Id); err != nil { + return nil, fmt.Errorf("删除AI平台失败: %v", err) + } + + return &types.Response{ + Code: 0, + Message: "success", + Success: true, + }, nil +} diff --git a/backend/internal/logic/ai/aiproviderlistlogic.go b/backend/internal/logic/ai/aiproviderlistlogic.go new file mode 100644 index 0000000..8e12236 --- /dev/null +++ b/backend/internal/logic/ai/aiproviderlistlogic.go @@ -0,0 +1,58 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "context" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AiProviderListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 获取AI平台列表 +func NewAiProviderListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiProviderListLogic { + return &AiProviderListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AiProviderListLogic) AiProviderList(req *types.AIProviderListRequest) (resp *types.AIProviderListResponse, err error) { + providers, total, err := model.AIProviderFindList(l.ctx, l.svcCtx.DB, req.Page, req.PageSize) + if err != nil { + return nil, fmt.Errorf("查询AI平台列表失败: %v", err) + } + + list := make([]types.AIProviderInfo, 0, len(providers)) + for _, p := range providers { + list = append(list, types.AIProviderInfo{ + Id: p.Id, + Name: p.Name, + DisplayName: p.DisplayName, + BaseUrl: p.BaseUrl, + SdkType: p.SdkType, + Protocol: p.Protocol, + IsActive: p.IsActive, + SortOrder: p.SortOrder, + CreatedAt: p.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: p.UpdatedAt.Format("2006-01-02 15:04:05"), + }) + } + + return &types.AIProviderListResponse{ + List: list, + Total: total, + }, nil +} diff --git a/backend/internal/logic/ai/aiproviderupdatelogic.go b/backend/internal/logic/ai/aiproviderupdatelogic.go new file mode 100644 index 0000000..dbc44c8 --- /dev/null +++ b/backend/internal/logic/ai/aiproviderupdatelogic.go @@ -0,0 +1,61 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package ai + +import ( + "context" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AiProviderUpdateLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 更新AI平台 +func NewAiProviderUpdateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiProviderUpdateLogic { + return &AiProviderUpdateLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AiProviderUpdateLogic) AiProviderUpdate(req *types.AIProviderUpdateRequest) (resp *types.AIProviderInfo, err error) { + provider, err := model.AIProviderFindOne(l.ctx, l.svcCtx.DB, req.Id) + if err != nil { + return nil, fmt.Errorf("AI平台不存在: %v", err) + } + + provider.DisplayName = req.DisplayName + provider.BaseUrl = req.BaseUrl + provider.SdkType = req.SdkType + provider.Protocol = req.Protocol + provider.IsActive = req.IsActive + provider.SortOrder = req.SortOrder + + if err := model.AIProviderUpdate(l.ctx, l.svcCtx.DB, provider); err != nil { + return nil, fmt.Errorf("更新AI平台失败: %v", err) + } + + return &types.AIProviderInfo{ + Id: provider.Id, + Name: provider.Name, + DisplayName: provider.DisplayName, + BaseUrl: provider.BaseUrl, + SdkType: provider.SdkType, + Protocol: provider.Protocol, + IsActive: provider.IsActive, + SortOrder: provider.SortOrder, + CreatedAt: provider.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: provider.UpdatedAt.Format("2006-01-02 15:04:05"), + }, nil +} diff --git a/backend/internal/svc/servicecontext.go b/backend/internal/svc/servicecontext.go index 4545392..45e549d 100644 --- a/backend/internal/svc/servicecontext.go +++ b/backend/internal/svc/servicecontext.go @@ -275,6 +275,21 @@ func seedCasbinPolicies(enforcer *casbin.Enforcer) { {"user", "/api/v1/ai/conversation/:id", "DELETE"}, {"user", "/api/v1/ai/models", "GET"}, {"user", "/api/v1/ai/quota/me", "GET"}, + + // AI: user API key management + {"user", "/api/v1/ai/keys", "GET"}, + {"user", "/api/v1/ai/key", "POST"}, + {"user", "/api/v1/ai/key/:id", "PUT"}, + {"user", "/api/v1/ai/key/:id", "DELETE"}, + + // AI: admin provider/model management + {"admin", "/api/v1/ai/providers", "GET"}, + {"admin", "/api/v1/ai/provider", "POST"}, + {"admin", "/api/v1/ai/provider/:id", "PUT"}, + {"admin", "/api/v1/ai/provider/:id", "DELETE"}, + {"admin", "/api/v1/ai/model", "POST"}, + {"admin", "/api/v1/ai/model/:id", "PUT"}, + {"admin", "/api/v1/ai/model/:id", "DELETE"}, } for _, p := range policies { @@ -312,11 +327,13 @@ func seedMenus(db *gorm.DB) { {Name: "仪表盘", Path: "/dashboard", Icon: "LayoutDashboard", Type: "config", SortOrder: 2, Visible: true, Status: 1}, {Name: "用户管理", Path: "/users", Icon: "Users", Type: "config", SortOrder: 3, Visible: true, Status: 1}, {Name: "文件管理", Path: "/files", Icon: "FolderOpen", Type: "config", SortOrder: 4, Visible: true, Status: 1}, - {Name: "AI 对话", Path: "/ai/chat", Icon: "Bot", Type: "config", SortOrder: 5, Visible: true, Status: 1}, - {Name: "角色管理", Path: "/roles", Icon: "Shield", Type: "config", SortOrder: 6, Visible: true, Status: 1}, - {Name: "菜单管理", Path: "/menus", Icon: "Menu", Type: "config", SortOrder: 7, Visible: true, Status: 1}, - {Name: "机构管理", Path: "/organizations", Icon: "Building2", Type: "config", SortOrder: 8, Visible: true, Status: 1}, - {Name: "设置", Path: "/settings", Icon: "Settings", Type: "default", SortOrder: 9, Visible: true, Status: 1}, + {Name: "AI 对话", Path: "/ai/chat", Icon: "MessageSquare", Type: "config", SortOrder: 5, Visible: true, Status: 1}, + {Name: "AI 模型", Path: "/ai/models", Icon: "Cpu", Type: "config", SortOrder: 6, Visible: true, Status: 1}, + {Name: "API 密钥", Path: "/ai/keys", Icon: "Key", Type: "config", SortOrder: 7, Visible: true, Status: 1}, + {Name: "角色管理", Path: "/roles", Icon: "Shield", Type: "config", SortOrder: 8, Visible: true, Status: 1}, + {Name: "菜单管理", Path: "/menus", Icon: "Menu", Type: "config", SortOrder: 9, Visible: true, Status: 1}, + {Name: "机构管理", Path: "/organizations", Icon: "Building2", Type: "config", SortOrder: 10, Visible: true, Status: 1}, + {Name: "设置", Path: "/settings", Icon: "Settings", Type: "default", SortOrder: 11, Visible: true, Status: 1}, } for _, m := range menus { diff --git a/backend/internal/types/types.go b/backend/internal/types/types.go index 87d6eb5..ab5e0a6 100644 --- a/backend/internal/types/types.go +++ b/backend/internal/types/types.go @@ -3,6 +3,44 @@ package types +type AIApiKeyCreateRequest struct { + ProviderId int64 `json:"providerId,string"` + KeyValue string `json:"keyValue"` + Remark string `json:"remark,optional"` +} + +type AIApiKeyDeleteRequest struct { + Id int64 `path:"id"` +} + +type AIApiKeyInfo struct { + Id int64 `json:"id,string"` + ProviderId int64 `json:"providerId,string"` + ProviderName string `json:"providerName"` + UserId int64 `json:"userId,string"` + KeyPreview string `json:"keyPreview"` + IsActive bool `json:"isActive"` + Remark string `json:"remark"` + CreatedAt string `json:"createdAt"` +} + +type AIApiKeyListRequest struct { + Page int64 `form:"page,optional,default=1"` + PageSize int64 `form:"pageSize,optional,default=20"` +} + +type AIApiKeyListResponse struct { + List []AIApiKeyInfo `json:"list"` + Total int64 `json:"total"` +} + +type AIApiKeyUpdateRequest struct { + Id int64 `path:"id"` + KeyValue string `json:"keyValue,optional"` + IsActive bool `json:"isActive,optional"` + Remark string `json:"remark,optional"` +} + type AIChatCompletionChoice struct { Index int `json:"index"` FinishReason string `json:"finish_reason"` @@ -94,6 +132,22 @@ type AIMessageInfo struct { CreatedAt string `json:"createdAt"` } +type AIModelCreateRequest struct { + ProviderId int64 `json:"providerId,string"` + ModelId string `json:"modelId"` + DisplayName string `json:"displayName"` + InputPrice float64 `json:"inputPrice"` + OutputPrice float64 `json:"outputPrice"` + MaxTokens int `json:"maxTokens,optional,default=4096"` + ContextWindow int `json:"contextWindow,optional,default=128000"` + SupportsStream bool `json:"supportsStream,optional"` + SupportsVision bool `json:"supportsVision,optional"` +} + +type AIModelDeleteRequest struct { + Id int64 `path:"id"` +} + type AIModelInfo struct { Id int64 `json:"id,string"` ProviderId int64 `json:"providerId,string"` @@ -112,6 +166,64 @@ type AIModelListResponse struct { List []AIModelInfo `json:"list"` } +type AIModelUpdateRequest struct { + Id int64 `path:"id"` + DisplayName string `json:"displayName,optional"` + InputPrice float64 `json:"inputPrice,optional"` + OutputPrice float64 `json:"outputPrice,optional"` + MaxTokens int `json:"maxTokens,optional"` + ContextWindow int `json:"contextWindow,optional"` + SupportsStream bool `json:"supportsStream,optional"` + SupportsVision bool `json:"supportsVision,optional"` + IsActive bool `json:"isActive,optional"` +} + +type AIProviderCreateRequest struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + BaseUrl string `json:"baseUrl"` + SdkType string `json:"sdkType"` + Protocol string `json:"protocol,optional,default=openai"` + SortOrder int `json:"sortOrder,optional"` +} + +type AIProviderDeleteRequest struct { + Id int64 `path:"id"` +} + +type AIProviderInfo struct { + Id int64 `json:"id,string"` + Name string `json:"name"` + DisplayName string `json:"displayName"` + BaseUrl string `json:"baseUrl"` + SdkType string `json:"sdkType"` + Protocol string `json:"protocol"` + IsActive bool `json:"isActive"` + SortOrder int `json:"sortOrder"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +type AIProviderListRequest struct { + Page int64 `form:"page,optional,default=1"` + PageSize int64 `form:"pageSize,optional,default=50"` +} + +type AIProviderListResponse struct { + List []AIProviderInfo `json:"list"` + Total int64 `json:"total"` +} + +type AIProviderUpdateRequest struct { + Id int64 `path:"id"` + DisplayName string `json:"displayName,optional"` + BaseUrl string `json:"baseUrl,optional"` + SdkType string `json:"sdkType,optional"` + Protocol string `json:"protocol,optional"` + IsActive bool `json:"isActive,optional"` + SortOrder int `json:"sortOrder,optional"` +} + type AIQuotaInfo struct { Balance float64 `json:"balance"` TotalRecharged float64 `json:"totalRecharged"` diff --git a/frontend/react-shadcn/pc/src/App.tsx b/frontend/react-shadcn/pc/src/App.tsx index b9fdd13..aef47c3 100644 --- a/frontend/react-shadcn/pc/src/App.tsx +++ b/frontend/react-shadcn/pc/src/App.tsx @@ -15,6 +15,8 @@ import { MenuManagementPage } from './pages/MenuManagementPage' import { RoleManagementPage } from './pages/RoleManagementPage' import { OrganizationManagementPage } from './pages/OrganizationManagementPage' import { AIChatPage } from './pages/AIChatPage' +import { AIModelManagementPage } from './pages/AIModelManagementPage' +import { AIKeyManagementPage } from './pages/AIKeyManagementPage' function App() { return ( @@ -42,6 +44,8 @@ function App() { } /> } /> } /> + } /> + } /> diff --git a/frontend/react-shadcn/pc/src/components/layout/MainLayout.tsx b/frontend/react-shadcn/pc/src/components/layout/MainLayout.tsx index b70c9fa..fb5a2d5 100644 --- a/frontend/react-shadcn/pc/src/components/layout/MainLayout.tsx +++ b/frontend/react-shadcn/pc/src/components/layout/MainLayout.tsx @@ -12,6 +12,8 @@ const pageTitles: Record = { '/organizations': { title: '机构管理', subtitle: '管理组织架构与成员' }, '/settings': { title: '系统设置', subtitle: '配置系统参数' }, '/ai/chat': { title: 'AI 对话', subtitle: '智能助手' }, + '/ai/models': { title: 'AI 模型', subtitle: '管理平台与模型配置' }, + '/ai/keys': { title: 'API 密钥', subtitle: '管理 API 访问密钥' }, } export function MainLayout() { diff --git a/frontend/react-shadcn/pc/src/components/layout/Sidebar.tsx b/frontend/react-shadcn/pc/src/components/layout/Sidebar.tsx index 16592c3..fc2eb06 100644 --- a/frontend/react-shadcn/pc/src/components/layout/Sidebar.tsx +++ b/frontend/react-shadcn/pc/src/components/layout/Sidebar.tsx @@ -2,6 +2,7 @@ import { NavLink } from 'react-router-dom' import { LayoutDashboard, Users, LogOut, Settings, FolderOpen, Shield, Menu as MenuIcon, Building2, User, ChevronDown, + Cpu, Key, MessageSquare, } from 'lucide-react' import type { LucideIcon } from 'lucide-react' import { useAuth } from '@/contexts/AuthContext' @@ -11,6 +12,7 @@ import type { MenuItem } from '@/types' const iconMap: Record = { User, LayoutDashboard, Users, FolderOpen, Shield, Menu: MenuIcon, Building2, Settings, + Cpu, Key, MessageSquare, } function getIcon(iconName: string): LucideIcon { diff --git a/frontend/react-shadcn/pc/src/pages/AIKeyManagementPage.tsx b/frontend/react-shadcn/pc/src/pages/AIKeyManagementPage.tsx new file mode 100644 index 0000000..e0271ec --- /dev/null +++ b/frontend/react-shadcn/pc/src/pages/AIKeyManagementPage.tsx @@ -0,0 +1,469 @@ +import { useState, useEffect, useCallback } from 'react' +import { Plus, Search, Edit2, Trash2, Key, Eye, EyeOff } from 'lucide-react' +import { Button } from '@/components/ui/Button' +import { Input } from '@/components/ui/Input' +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card' +import { Modal } from '@/components/ui/Modal' +import { + Table, + TableHeader, + TableBody, + TableRow, + TableHead, + TableCell, +} from '@/components/ui/Table' +import type { AIApiKeyInfo, AIApiKeyCreateRequest, AIApiKeyUpdateRequest, AIProviderInfo } from '@/types' +import { apiClient } from '@/services/api' + +// --- Main Page Component --- + +export function AIKeyManagementPage() { + const [apiKeys, setApiKeys] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [searchQuery, setSearchQuery] = useState('') + + // Pagination + const [currentPage, setCurrentPage] = useState(1) + const [pageSize] = useState(10) + const [total, setTotal] = useState(0) + + // Create/Edit modal + const [isModalOpen, setIsModalOpen] = useState(false) + const [editingKey, setEditingKey] = useState(null) + const [formData, setFormData] = useState>({ + providerId: undefined, + keyValue: '', + remark: '', + }) + const [providers, setProviders] = useState([]) + const [isLoadingProviders, setIsLoadingProviders] = useState(false) + + // Delete confirmation + const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) + const [keyToDelete, setKeyToDelete] = useState(null) + const [isDeleting, setIsDeleting] = useState(false) + + // Password visibility + const [visibleKeys, setVisibleKeys] = useState>(new Set()) + + // Fetch API keys + const fetchApiKeys = useCallback(async () => { + try { + setIsLoading(true) + setError(null) + const response = await apiClient.getAIApiKeys(currentPage, pageSize) + setApiKeys(response.list || []) + setTotal(response.total || 0) + } catch (err) { + console.error('Failed to fetch API keys:', err) + setError('获取密钥列表失败,请稍后重试') + setApiKeys([]) + } finally { + setIsLoading(false) + } + }, [currentPage, pageSize]) + + useEffect(() => { + fetchApiKeys() + }, [fetchApiKeys]) + + // Fetch providers for the dropdown + const fetchProviders = async () => { + try { + setIsLoadingProviders(true) + const response = await apiClient.getAIProviders() + setProviders(response.list || []) + } catch (err) { + console.error('Failed to fetch providers:', err) + alert('加载平台列表失败') + setProviders([]) + } finally { + setIsLoadingProviders(false) + } + } + + // --- Create / Edit --- + + const openModal = async (key?: AIApiKeyInfo) => { + await fetchProviders() + if (key) { + setEditingKey(key) + setFormData({ + providerId: key.providerId, + keyValue: '', + remark: key.remark, + }) + } else { + setEditingKey(null) + setFormData({ providerId: undefined, keyValue: '', remark: '' }) + } + setIsModalOpen(true) + } + + const resetForm = () => { + setFormData({ providerId: undefined, keyValue: '', remark: '' }) + setEditingKey(null) + } + + const handleCreateKey = async () => { + try { + if (!formData.providerId || !formData.keyValue) { + alert('请填写必填字段') + return + } + await apiClient.createAIApiKey(formData as AIApiKeyCreateRequest) + await fetchApiKeys() + setIsModalOpen(false) + resetForm() + } catch (err) { + console.error('Failed to create API key:', err) + alert('创建密钥失败') + } + } + + const handleUpdateKey = async () => { + if (!editingKey) return + try { + const data: AIApiKeyUpdateRequest = { + keyValue: formData.keyValue || undefined, + isActive: formData.isActive, + remark: formData.remark, + } + await apiClient.updateAIApiKey(editingKey.id, data) + await fetchApiKeys() + setIsModalOpen(false) + resetForm() + } catch (err) { + console.error('Failed to update API key:', err) + alert('更新密钥失败') + } + } + + // --- Delete --- + + const openDeleteConfirm = (key: AIApiKeyInfo) => { + setKeyToDelete(key) + setDeleteConfirmOpen(true) + } + + const handleDeleteKey = async () => { + if (!keyToDelete) return + try { + setIsDeleting(true) + await apiClient.deleteAIApiKey(keyToDelete.id) + setDeleteConfirmOpen(false) + setKeyToDelete(null) + await fetchApiKeys() + } catch (err) { + console.error('Failed to delete API key:', err) + alert('删除密钥失败') + } finally { + setIsDeleting(false) + } + } + + // --- Password visibility toggle --- + + const toggleKeyVisibility = (keyId: number) => { + setVisibleKeys((prev) => { + const next = new Set(prev) + if (next.has(keyId)) { + next.delete(keyId) + } else { + next.add(keyId) + } + return next + }) + } + + // --- Filtering --- + + const filteredKeys = apiKeys.filter( + (key) => + key.providerName.toLowerCase().includes(searchQuery.toLowerCase()) || + (key.remark && key.remark.toLowerCase().includes(searchQuery.toLowerCase())) + ) + + // --- Format date --- + + const formatDate = (dateString: string) => { + try { + const date = new Date(dateString) + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }) + } catch { + return dateString + } + } + + // --- Render --- + + return ( +
+ {/* Header */} + + +
+
+ setSearchQuery(e.target.value)} + leftIcon={} + /> +
+ +
+
+
+ + {/* Error Message */} + {error && ( +
+ {error} + +
+ )} + + {/* API Keys Table */} + + + 密钥列表 ({filteredKeys.length}) + + +
+ + + + 平台 + 密钥预览 + 状态 + 备注 + 创建时间 + 操作 + + + + {isLoading ? ( + + 加载中... + + ) : filteredKeys.length === 0 ? ( + + 暂无数据 + + ) : ( + filteredKeys.map((key) => ( + + +
+ + {key.providerName} +
+
+ +
+ + {visibleKeys.has(key.id) ? key.keyValue : key.keyPreview} + + +
+
+ + {key.isActive ? ( + + 启用 + + ) : ( + + 禁用 + + )} + + + {key.remark || '-'} + + + {formatDate(key.createdAt)} + + +
+ + +
+
+
+ )) + )} +
+
+
+
+
+ + {/* Create / Edit Modal */} + { + setIsModalOpen(false) + resetForm() + }} + title={editingKey ? '编辑密钥' : '新建密钥'} + size="md" + footer={ + <> + + + + } + > +
+ {!editingKey && ( +
+ + {isLoadingProviders ? ( +
加载中...
+ ) : ( + + )} +
+ )} + + setFormData({ ...formData, keyValue: e.target.value })} + type="password" + required={!editingKey} + /> + + {editingKey && ( +
+ +
+ )} + + setFormData({ ...formData, remark: e.target.value })} + /> +
+
+ + {/* Delete Confirmation Modal */} + { + setDeleteConfirmOpen(false) + setKeyToDelete(null) + }} + title="确认删除" + size="sm" + footer={ + <> + + + + } + > +
+

+ 确定要删除平台{' '} + {keyToDelete?.providerName}{' '} + 的密钥吗? +

+

此操作不可恢复,请谨慎操作。

+
+
+
+ ) +} diff --git a/frontend/react-shadcn/pc/src/pages/AIModelManagementPage.tsx b/frontend/react-shadcn/pc/src/pages/AIModelManagementPage.tsx new file mode 100644 index 0000000..509781a --- /dev/null +++ b/frontend/react-shadcn/pc/src/pages/AIModelManagementPage.tsx @@ -0,0 +1,988 @@ +import { useState, useEffect, useCallback } from 'react' +import { Plus, Search, Edit2, Trash2, Server, Cpu } from 'lucide-react' +import { Button } from '@/components/ui/Button' +import { Input } from '@/components/ui/Input' +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card' +import { Modal } from '@/components/ui/Modal' +import { + Table, + TableHeader, + TableBody, + TableRow, + TableHead, + TableCell, +} from '@/components/ui/Table' +import type { + AIProviderInfo, + AIProviderCreateRequest, + AIProviderUpdateRequest, + AIModelInfo, + AIModelCreateRequest, + AIModelUpdateRequest, +} from '@/types' +import { apiClient } from '@/services/api' + +// --- Main Page Component --- + +export function AIModelManagementPage() { + // Tab state + const [activeTab, setActiveTab] = useState<'providers' | 'models'>('providers') + + // Providers state + const [providers, setProviders] = useState([]) + const [isLoadingProviders, setIsLoadingProviders] = useState(true) + const [providerError, setProviderError] = useState(null) + const [providerSearchQuery, setProviderSearchQuery] = useState('') + const [providerPage, setProviderPage] = useState(1) + const [providerTotal, setProviderTotal] = useState(0) + const providerPageSize = 20 + + // Models state + const [models, setModels] = useState([]) + const [isLoadingModels, setIsLoadingModels] = useState(true) + const [modelError, setModelError] = useState(null) + const [modelSearchQuery, setModelSearchQuery] = useState('') + + // Provider Create/Edit modal + const [isProviderModalOpen, setIsProviderModalOpen] = useState(false) + const [editingProvider, setEditingProvider] = useState(null) + const [providerFormData, setProviderFormData] = useState>({ + name: '', + displayName: '', + baseUrl: '', + sdkType: 'openai', + protocol: 'openai', + sortOrder: 0, + }) + + // Model Create/Edit modal + const [isModelModalOpen, setIsModelModalOpen] = useState(false) + const [editingModel, setEditingModel] = useState(null) + const [modelFormData, setModelFormData] = useState>({ + providerId: 0, + modelId: '', + displayName: '', + inputPrice: 0, + outputPrice: 0, + maxTokens: 0, + contextWindow: 0, + supportsStream: false, + supportsVision: false, + }) + + // Delete confirmation + const [deleteProviderConfirmOpen, setDeleteProviderConfirmOpen] = useState(false) + const [providerToDelete, setProviderToDelete] = useState(null) + const [isDeletingProvider, setIsDeletingProvider] = useState(false) + + const [deleteModelConfirmOpen, setDeleteModelConfirmOpen] = useState(false) + const [modelToDelete, setModelToDelete] = useState(null) + const [isDeletingModel, setIsDeletingModel] = useState(false) + + // --- Fetch Providers --- + + const fetchProviders = useCallback(async () => { + try { + setIsLoadingProviders(true) + setProviderError(null) + const response = await apiClient.getAIProviders(providerPage, providerPageSize) + setProviders(response.list || []) + setProviderTotal(response.total || 0) + } catch (err) { + console.error('Failed to fetch providers:', err) + setProviderError('获取AI平台列表失败,请稍后重试') + setProviders([]) + } finally { + setIsLoadingProviders(false) + } + }, [providerPage, providerPageSize]) + + useEffect(() => { + if (activeTab === 'providers') { + fetchProviders() + } + }, [activeTab, fetchProviders]) + + // --- Fetch Models --- + + const fetchModels = useCallback(async () => { + try { + setIsLoadingModels(true) + setModelError(null) + const response = await apiClient.getAIModels() + setModels(response.list || []) + } catch (err) { + console.error('Failed to fetch models:', err) + setModelError('获取AI模型列表失败,请稍后重试') + setModels([]) + } finally { + setIsLoadingModels(false) + } + }, []) + + useEffect(() => { + if (activeTab === 'models') { + fetchModels() + // Also fetch providers for the dropdown + if (providers.length === 0) { + fetchProviders() + } + } + }, [activeTab, fetchModels, providers.length, fetchProviders]) + + // --- Provider Create / Edit --- + + const openProviderModal = (provider?: AIProviderInfo) => { + if (provider) { + setEditingProvider(provider) + setProviderFormData({ + name: provider.name, + displayName: provider.displayName, + baseUrl: provider.baseUrl, + sdkType: provider.sdkType, + protocol: provider.protocol, + sortOrder: provider.sortOrder, + isActive: provider.isActive, + }) + } else { + setEditingProvider(null) + setProviderFormData({ + name: '', + displayName: '', + baseUrl: '', + sdkType: 'openai', + protocol: 'openai', + sortOrder: 0, + }) + } + setIsProviderModalOpen(true) + } + + const resetProviderForm = () => { + setProviderFormData({ + name: '', + displayName: '', + baseUrl: '', + sdkType: 'openai', + protocol: 'openai', + sortOrder: 0, + }) + setEditingProvider(null) + } + + const handleCreateProvider = async () => { + try { + await apiClient.createAIProvider(providerFormData as AIProviderCreateRequest) + await fetchProviders() + setIsProviderModalOpen(false) + resetProviderForm() + } catch (err) { + console.error('Failed to create provider:', err) + alert('创建AI平台失败') + } + } + + const handleUpdateProvider = async () => { + if (!editingProvider) return + try { + const data: AIProviderUpdateRequest = { + displayName: providerFormData.displayName, + baseUrl: providerFormData.baseUrl, + sdkType: providerFormData.sdkType, + protocol: providerFormData.protocol, + isActive: providerFormData.isActive, + sortOrder: providerFormData.sortOrder, + } + await apiClient.updateAIProvider(editingProvider.id, data) + await fetchProviders() + setIsProviderModalOpen(false) + resetProviderForm() + } catch (err) { + console.error('Failed to update provider:', err) + alert('更新AI平台失败') + } + } + + // --- Provider Delete --- + + const openProviderDeleteConfirm = (provider: AIProviderInfo) => { + setProviderToDelete(provider) + setDeleteProviderConfirmOpen(true) + } + + const handleDeleteProvider = async () => { + if (!providerToDelete) return + try { + setIsDeletingProvider(true) + await apiClient.deleteAIProvider(providerToDelete.id) + setDeleteProviderConfirmOpen(false) + setProviderToDelete(null) + await fetchProviders() + } catch (err) { + console.error('Failed to delete provider:', err) + alert('删除AI平台失败') + } finally { + setIsDeletingProvider(false) + } + } + + // --- Model Create / Edit --- + + const openModelModal = (model?: AIModelInfo) => { + if (model) { + setEditingModel(model) + setModelFormData({ + providerId: model.providerId, + modelId: model.modelId, + displayName: model.displayName, + inputPrice: model.inputPrice, + outputPrice: model.outputPrice, + maxTokens: model.maxTokens, + contextWindow: model.contextWindow, + supportsStream: model.supportsStream, + supportsVision: model.supportsVision, + }) + } else { + setEditingModel(null) + setModelFormData({ + providerId: providers.length > 0 ? providers[0].id : 0, + modelId: '', + displayName: '', + inputPrice: 0, + outputPrice: 0, + maxTokens: 0, + contextWindow: 0, + supportsStream: false, + supportsVision: false, + }) + } + setIsModelModalOpen(true) + } + + const resetModelForm = () => { + setModelFormData({ + providerId: 0, + modelId: '', + displayName: '', + inputPrice: 0, + outputPrice: 0, + maxTokens: 0, + contextWindow: 0, + supportsStream: false, + supportsVision: false, + }) + setEditingModel(null) + } + + const handleCreateModel = async () => { + try { + await apiClient.createAIModel(modelFormData as AIModelCreateRequest) + await fetchModels() + setIsModelModalOpen(false) + resetModelForm() + } catch (err) { + console.error('Failed to create model:', err) + alert('创建AI模型失败') + } + } + + const handleUpdateModel = async () => { + if (!editingModel) return + try { + const data: AIModelUpdateRequest = { + displayName: modelFormData.displayName, + inputPrice: modelFormData.inputPrice, + outputPrice: modelFormData.outputPrice, + maxTokens: modelFormData.maxTokens, + contextWindow: modelFormData.contextWindow, + supportsStream: modelFormData.supportsStream, + supportsVision: modelFormData.supportsVision, + isActive: modelFormData.isActive, + } + await apiClient.updateAIModel(editingModel.id, data) + await fetchModels() + setIsModelModalOpen(false) + resetModelForm() + } catch (err) { + console.error('Failed to update model:', err) + alert('更新AI模型失败') + } + } + + // --- Model Delete --- + + const openModelDeleteConfirm = (model: AIModelInfo) => { + setModelToDelete(model) + setDeleteModelConfirmOpen(true) + } + + const handleDeleteModel = async () => { + if (!modelToDelete) return + try { + setIsDeletingModel(true) + await apiClient.deleteAIModel(modelToDelete.id) + setDeleteModelConfirmOpen(false) + setModelToDelete(null) + await fetchModels() + } catch (err) { + console.error('Failed to delete model:', err) + alert('删除AI模型失败') + } finally { + setIsDeletingModel(false) + } + } + + // --- Filtering --- + + const filteredProviders = providers.filter( + (provider) => + provider.name.toLowerCase().includes(providerSearchQuery.toLowerCase()) || + provider.displayName.toLowerCase().includes(providerSearchQuery.toLowerCase()) || + provider.baseUrl.toLowerCase().includes(providerSearchQuery.toLowerCase()) + ) + + const filteredModels = models.filter( + (model) => + model.modelId.toLowerCase().includes(modelSearchQuery.toLowerCase()) || + model.displayName.toLowerCase().includes(modelSearchQuery.toLowerCase()) || + model.providerName.toLowerCase().includes(modelSearchQuery.toLowerCase()) + ) + + // --- Format currency --- + const formatPrice = (price: number) => { + return `$${price.toFixed(2)}/1K` + } + + // --- Render --- + + return ( +
+ {/* Tabs */} + + +
+ + +
+
+
+ + {/* Providers Tab */} + {activeTab === 'providers' && ( + <> + {/* Header */} + + +
+
+ setProviderSearchQuery(e.target.value)} + leftIcon={} + /> +
+ +
+
+
+ + {/* Error Message */} + {providerError && ( +
+ {providerError} + +
+ )} + + {/* Providers Table */} + + + 平台列表 ({filteredProviders.length}) + + +
+ + + + 名称 + 显示名 + 基础URL + SDK类型 + 协议 + 状态 + 排序 + 操作 + + + + {isLoadingProviders ? ( + + 加载中... + + ) : filteredProviders.length === 0 ? ( + + 暂无数据 + + ) : ( + filteredProviders.map((provider) => ( + + + {provider.name} + + + {provider.displayName} + + + {provider.baseUrl} + + + + {provider.sdkType} + + + + + {provider.protocol} + + + + {provider.isActive ? ( + + 启用 + + ) : ( + + 禁用 + + )} + + + {provider.sortOrder} + + +
+ + +
+
+
+ )) + )} +
+
+
+
+
+ + )} + + {/* Models Tab */} + {activeTab === 'models' && ( + <> + {/* Header */} + + +
+
+ setModelSearchQuery(e.target.value)} + leftIcon={} + /> +
+ +
+
+
+ + {/* Error Message */} + {modelError && ( +
+ {modelError} + +
+ )} + + {/* Models Table */} + + + 模型列表 ({filteredModels.length}) + + +
+ + + + 模型ID + 显示名 + 所属平台 + 输入价格 + 输出价格 + 最大Token + 上下文 + 流式 + 视觉 + 操作 + + + + {isLoadingModels ? ( + + 加载中... + + ) : filteredModels.length === 0 ? ( + + 暂无数据 + + ) : ( + filteredModels.map((model) => ( + + + {model.modelId} + + + {model.displayName} + + + + {model.providerName} + + + + {formatPrice(model.inputPrice)} + + + {formatPrice(model.outputPrice)} + + + {model.maxTokens.toLocaleString()} + + + {model.contextWindow.toLocaleString()} + + + {model.supportsStream ? ( + + ✓ + + ) : ( + + - + + )} + + + {model.supportsVision ? ( + + ✓ + + ) : ( + + - + + )} + + +
+ + +
+
+
+ )) + )} +
+
+
+
+
+ + )} + + {/* Provider Create / Edit Modal */} + { + setIsProviderModalOpen(false) + resetProviderForm() + }} + title={editingProvider ? '编辑AI平台' : '新建AI平台'} + size="md" + footer={ + <> + + + + } + > +
+ setProviderFormData({ ...providerFormData, name: e.target.value })} + disabled={!!editingProvider} + /> + + setProviderFormData({ ...providerFormData, displayName: e.target.value }) + } + /> + setProviderFormData({ ...providerFormData, baseUrl: e.target.value })} + /> +
+ + +
+
+ + +
+ + setProviderFormData({ ...providerFormData, sortOrder: parseInt(e.target.value) || 0 }) + } + /> + {editingProvider && ( +
+ + setProviderFormData({ ...providerFormData, isActive: e.target.checked }) + } + className="h-4 w-4 rounded border-border-secondary bg-card text-sky-500 focus:ring-sky-500/50 focus:ring-offset-0 accent-sky-500" + /> + +
+ )} +
+
+ + {/* Model Create / Edit Modal */} + { + setIsModelModalOpen(false) + resetModelForm() + }} + title={editingModel ? '编辑AI模型' : '新建AI模型'} + size="md" + footer={ + <> + + + + } + > +
+ {!editingModel && ( +
+ + +
+ )} + setModelFormData({ ...modelFormData, modelId: e.target.value })} + disabled={!!editingModel} + /> + setModelFormData({ ...modelFormData, displayName: e.target.value })} + /> + + setModelFormData({ ...modelFormData, inputPrice: parseFloat(e.target.value) || 0 }) + } + /> + + setModelFormData({ ...modelFormData, outputPrice: parseFloat(e.target.value) || 0 }) + } + /> + + setModelFormData({ ...modelFormData, maxTokens: parseInt(e.target.value) || 0 }) + } + /> + + setModelFormData({ + ...modelFormData, + contextWindow: parseInt(e.target.value) || 0, + }) + } + /> +
+ + setModelFormData({ ...modelFormData, supportsStream: e.target.checked }) + } + className="h-4 w-4 rounded border-border-secondary bg-card text-sky-500 focus:ring-sky-500/50 focus:ring-offset-0 accent-sky-500" + /> + +
+
+ + setModelFormData({ ...modelFormData, supportsVision: e.target.checked }) + } + className="h-4 w-4 rounded border-border-secondary bg-card text-sky-500 focus:ring-sky-500/50 focus:ring-offset-0 accent-sky-500" + /> + +
+ {editingModel && ( +
+ + setModelFormData({ ...modelFormData, isActive: e.target.checked }) + } + className="h-4 w-4 rounded border-border-secondary bg-card text-sky-500 focus:ring-sky-500/50 focus:ring-offset-0 accent-sky-500" + /> + +
+ )} +
+
+ + {/* Provider Delete Confirmation Modal */} + { + setDeleteProviderConfirmOpen(false) + setProviderToDelete(null) + }} + title="确认删除" + size="sm" + footer={ + <> + + + + } + > +
+

+ 确定要删除平台{' '} + {providerToDelete?.displayName}{' '} + 吗? +

+

此操作不可恢复,请谨慎操作。

+
+
+ + {/* Model Delete Confirmation Modal */} + { + setDeleteModelConfirmOpen(false) + setModelToDelete(null) + }} + title="确认删除" + size="sm" + footer={ + <> + + + + } + > +
+

+ 确定要删除模型{' '} + {modelToDelete?.displayName} 吗? +

+

此操作不可恢复,请谨慎操作。

+
+
+
+ ) +} diff --git a/frontend/react-shadcn/pc/src/services/api.ts b/frontend/react-shadcn/pc/src/services/api.ts index 0819503..6f2e5a1 100644 --- a/frontend/react-shadcn/pc/src/services/api.ts +++ b/frontend/react-shadcn/pc/src/services/api.ts @@ -32,6 +32,14 @@ import type { AIChatMessage, AIQuotaInfo, AIChatCompletionRequest, + AIProviderInfo, + AIProviderCreateRequest, + AIProviderUpdateRequest, + AIModelCreateRequest, + AIModelUpdateRequest, + AIApiKeyInfo, + AIApiKeyCreateRequest, + AIApiKeyUpdateRequest, } from '@/types' const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8888/api/v1' @@ -490,6 +498,53 @@ class ApiClient { return this.request('/ai/quota/me') } + // AI Providers (admin) + async getAIProviders(page: number = 1, pageSize: number = 50): Promise<{ list: AIProviderInfo[]; total: number }> { + return this.request<{ list: AIProviderInfo[]; total: number }>(`/ai/providers?page=${page}&pageSize=${pageSize}`) + } + + async createAIProvider(data: AIProviderCreateRequest): Promise { + return this.request('/ai/provider', { method: 'POST', body: JSON.stringify(data) }) + } + + async updateAIProvider(id: number, data: AIProviderUpdateRequest): Promise { + return this.request(`/ai/provider/${id}`, { method: 'PUT', body: JSON.stringify(data) }) + } + + async deleteAIProvider(id: number): Promise { + await this.request(`/ai/provider/${id}`, { method: 'DELETE' }) + } + + // AI Models (admin) + async createAIModel(data: AIModelCreateRequest): Promise { + return this.request('/ai/model', { method: 'POST', body: JSON.stringify(data) }) + } + + async updateAIModel(id: number, data: AIModelUpdateRequest): Promise { + return this.request(`/ai/model/${id}`, { method: 'PUT', body: JSON.stringify(data) }) + } + + async deleteAIModel(id: number): Promise { + await this.request(`/ai/model/${id}`, { method: 'DELETE' }) + } + + // AI API Keys + async getAIApiKeys(page: number = 1, pageSize: number = 20): Promise<{ list: AIApiKeyInfo[]; total: number }> { + return this.request<{ list: AIApiKeyInfo[]; total: number }>(`/ai/keys?page=${page}&pageSize=${pageSize}`) + } + + async createAIApiKey(data: AIApiKeyCreateRequest): Promise { + return this.request('/ai/key', { method: 'POST', body: JSON.stringify(data) }) + } + + async updateAIApiKey(id: number, data: AIApiKeyUpdateRequest): Promise { + return this.request(`/ai/key/${id}`, { method: 'PUT', body: JSON.stringify(data) }) + } + + async deleteAIApiKey(id: number): Promise { + await this.request(`/ai/key/${id}`, { method: 'DELETE' }) + } + // Health check async healthCheck(): Promise<{ status: string }> { try { diff --git a/frontend/react-shadcn/pc/src/types/index.ts b/frontend/react-shadcn/pc/src/types/index.ts index 6d589e6..e96b463 100644 --- a/frontend/react-shadcn/pc/src/types/index.ts +++ b/frontend/react-shadcn/pc/src/types/index.ts @@ -339,3 +339,83 @@ export interface AIChatCompletionRequest { temperature?: number conversation_id?: number } + +// AI Provider Types +export interface AIProviderInfo { + id: number + name: string + displayName: string + baseUrl: string + sdkType: string + protocol: string + isActive: boolean + sortOrder: number + createdAt: string + updatedAt: string +} + +export interface AIProviderCreateRequest { + name: string + displayName: string + baseUrl: string + sdkType: string + protocol?: string + sortOrder?: number +} + +export interface AIProviderUpdateRequest { + displayName?: string + baseUrl?: string + sdkType?: string + protocol?: string + isActive?: boolean + sortOrder?: number +} + +// AI Model Admin Types +export interface AIModelCreateRequest { + providerId: number + modelId: string + displayName: string + inputPrice: number + outputPrice: number + maxTokens?: number + contextWindow?: number + supportsStream?: boolean + supportsVision?: boolean +} + +export interface AIModelUpdateRequest { + displayName?: string + inputPrice?: number + outputPrice?: number + maxTokens?: number + contextWindow?: number + supportsStream?: boolean + supportsVision?: boolean + isActive?: boolean +} + +// AI API Key Types +export interface AIApiKeyInfo { + id: number + providerId: number + providerName: string + userId: number + keyPreview: string + isActive: boolean + remark: string + createdAt: string +} + +export interface AIApiKeyCreateRequest { + providerId: number + keyValue: string + remark?: string +} + +export interface AIApiKeyUpdateRequest { + keyValue?: string + isActive?: boolean + remark?: string +}