Browse Source

feat: Phase 2 AI management CRUD — providers, models, API keys

Backend:
- Add provider/model/key types and routes to ai.api + base.api
- Generate 11 handler + logic stubs via goctl
- Implement provider CRUD logic (list/create/update/delete)
- Implement model admin CRUD logic (create/update/delete)
- Implement API key CRUD logic (list/create/update/delete)
- Add Casbin policies for key management and admin endpoints
- Add menu seeds with proper icons (Cpu, Key, MessageSquare)

Frontend:
- Add provider/model/key TypeScript types and API client methods
- Create AIModelManagementPage with provider + model tabs
- Create AIKeyManagementPage with key CRUD and provider dropdown
- Register routes and page titles for /ai/models and /ai/keys
- Add Cpu, Key, MessageSquare icons to Sidebar icon map
master
dark 1 month ago
parent
commit
899288180a
  1. 120
      backend/api/ai.api
  2. 52
      backend/base.api
  3. 32
      backend/internal/handler/ai/aiapikeycreatehandler.go
  4. 32
      backend/internal/handler/ai/aiapikeydeletehandler.go
  5. 32
      backend/internal/handler/ai/aiapikeylisthandler.go
  6. 32
      backend/internal/handler/ai/aiapikeyupdatehandler.go
  7. 32
      backend/internal/handler/ai/aimodelcreatehandler.go
  8. 32
      backend/internal/handler/ai/aimodeldeletehandler.go
  9. 32
      backend/internal/handler/ai/aimodelupdatehandler.go
  10. 32
      backend/internal/handler/ai/aiprovidercreatehandler.go
  11. 32
      backend/internal/handler/ai/aiproviderdeletehandler.go
  12. 32
      backend/internal/handler/ai/aiproviderlisthandler.go
  13. 32
      backend/internal/handler/ai/aiproviderupdatehandler.go
  14. 75
      backend/internal/handler/routes.go
  15. 73
      backend/internal/logic/ai/aiapikeycreatelogic.go
  16. 60
      backend/internal/logic/ai/aiapikeydeletelogic.go
  17. 90
      backend/internal/logic/ai/aiapikeylistlogic.go
  18. 79
      backend/internal/logic/ai/aiapikeyupdatelogic.go
  19. 79
      backend/internal/logic/ai/aimodelcreatelogic.go
  20. 44
      backend/internal/logic/ai/aimodeldeletelogic.go
  21. 76
      backend/internal/logic/ai/aimodelupdatelogic.go
  22. 60
      backend/internal/logic/ai/aiprovidercreatelogic.go
  23. 42
      backend/internal/logic/ai/aiproviderdeletelogic.go
  24. 58
      backend/internal/logic/ai/aiproviderlistlogic.go
  25. 61
      backend/internal/logic/ai/aiproviderupdatelogic.go
  26. 27
      backend/internal/svc/servicecontext.go
  27. 112
      backend/internal/types/types.go
  28. 4
      frontend/react-shadcn/pc/src/App.tsx
  29. 2
      frontend/react-shadcn/pc/src/components/layout/MainLayout.tsx
  30. 2
      frontend/react-shadcn/pc/src/components/layout/Sidebar.tsx
  31. 469
      frontend/react-shadcn/pc/src/pages/AIKeyManagementPage.tsx
  32. 988
      frontend/react-shadcn/pc/src/pages/AIModelManagementPage.tsx
  33. 55
      frontend/react-shadcn/pc/src/services/api.ts
  34. 80
      frontend/react-shadcn/pc/src/types/index.ts

120
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 ========== // ========== Model Types ==========
type ( type (
@ -119,6 +169,76 @@ type (
AIModelListResponse { AIModelListResponse {
List []AIModelInfo `json:"list"` 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 ========== // ========== Quota Types ==========

52
backend/base.api

@ -333,5 +333,57 @@ service base-api {
@doc "获取我的配额" @doc "获取我的配额"
@handler AiQuotaMe @handler AiQuotaMe
get /ai/quota/me returns (AIQuotaInfo) 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)
} }

32
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)
}
}
}

32
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)
}
}
}

32
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)
}
}
}

32
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)
}
}
}

32
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)
}
}
}

32
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)
}
}
}

32
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)
}
}
}

32
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)
}
}
}

32
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)
}
}
}

32
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)
}
}
}

32
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)
}
}
}

75
backend/internal/handler/routes.go

@ -61,6 +61,30 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/ai/conversations", Path: "/ai/conversations",
Handler: ai.AiConversationListHandler(serverCtx), 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, Method: http.MethodGet,
@ -78,6 +102,57 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
rest.WithPrefix("/api/v1"), 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( server.AddRoutes(
rest.WithMiddlewares( rest.WithMiddlewares(
[]rest.Middleware{serverCtx.Cors, serverCtx.Log}, []rest.Middleware{serverCtx.Cors, serverCtx.Log},

73
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
}

60
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
}

90
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:]
}

79
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
}

79
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
}

44
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
}

76
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
}

60
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
}

42
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
}

58
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
}

61
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
}

27
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/conversation/:id", "DELETE"},
{"user", "/api/v1/ai/models", "GET"}, {"user", "/api/v1/ai/models", "GET"},
{"user", "/api/v1/ai/quota/me", "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 { 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: "/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: "/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: "文件管理", 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: "AI 对话", Path: "/ai/chat", Icon: "MessageSquare", Type: "config", SortOrder: 5, Visible: true, Status: 1},
{Name: "角色管理", Path: "/roles", Icon: "Shield", Type: "config", SortOrder: 6, Visible: true, Status: 1}, {Name: "AI 模型", Path: "/ai/models", Icon: "Cpu", Type: "config", SortOrder: 6, Visible: true, Status: 1},
{Name: "菜单管理", Path: "/menus", Icon: "Menu", Type: "config", SortOrder: 7, Visible: true, Status: 1}, {Name: "API 密钥", Path: "/ai/keys", Icon: "Key", Type: "config", SortOrder: 7, Visible: true, Status: 1},
{Name: "机构管理", Path: "/organizations", Icon: "Building2", Type: "config", SortOrder: 8, Visible: true, Status: 1}, {Name: "角色管理", Path: "/roles", Icon: "Shield", Type: "config", SortOrder: 8, Visible: true, Status: 1},
{Name: "设置", Path: "/settings", Icon: "Settings", Type: "default", SortOrder: 9, 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 { for _, m := range menus {

112
backend/internal/types/types.go

@ -3,6 +3,44 @@
package types 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 { type AIChatCompletionChoice struct {
Index int `json:"index"` Index int `json:"index"`
FinishReason string `json:"finish_reason"` FinishReason string `json:"finish_reason"`
@ -94,6 +132,22 @@ type AIMessageInfo struct {
CreatedAt string `json:"createdAt"` 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 { type AIModelInfo struct {
Id int64 `json:"id,string"` Id int64 `json:"id,string"`
ProviderId int64 `json:"providerId,string"` ProviderId int64 `json:"providerId,string"`
@ -112,6 +166,64 @@ type AIModelListResponse struct {
List []AIModelInfo `json:"list"` 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 { type AIQuotaInfo struct {
Balance float64 `json:"balance"` Balance float64 `json:"balance"`
TotalRecharged float64 `json:"totalRecharged"` TotalRecharged float64 `json:"totalRecharged"`

4
frontend/react-shadcn/pc/src/App.tsx

@ -15,6 +15,8 @@ import { MenuManagementPage } from './pages/MenuManagementPage'
import { RoleManagementPage } from './pages/RoleManagementPage' import { RoleManagementPage } from './pages/RoleManagementPage'
import { OrganizationManagementPage } from './pages/OrganizationManagementPage' import { OrganizationManagementPage } from './pages/OrganizationManagementPage'
import { AIChatPage } from './pages/AIChatPage' import { AIChatPage } from './pages/AIChatPage'
import { AIModelManagementPage } from './pages/AIModelManagementPage'
import { AIKeyManagementPage } from './pages/AIKeyManagementPage'
function App() { function App() {
return ( return (
@ -42,6 +44,8 @@ function App() {
<Route path="roles" element={<RouteGuard><RoleManagementPage /></RouteGuard>} /> <Route path="roles" element={<RouteGuard><RoleManagementPage /></RouteGuard>} />
<Route path="organizations" element={<RouteGuard><OrganizationManagementPage /></RouteGuard>} /> <Route path="organizations" element={<RouteGuard><OrganizationManagementPage /></RouteGuard>} />
<Route path="ai/chat" element={<RouteGuard><AIChatPage /></RouteGuard>} /> <Route path="ai/chat" element={<RouteGuard><AIChatPage /></RouteGuard>} />
<Route path="ai/models" element={<RouteGuard><AIModelManagementPage /></RouteGuard>} />
<Route path="ai/keys" element={<RouteGuard><AIKeyManagementPage /></RouteGuard>} />
</Route> </Route>
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>

2
frontend/react-shadcn/pc/src/components/layout/MainLayout.tsx

@ -12,6 +12,8 @@ const pageTitles: Record<string, { title: string; subtitle?: string }> = {
'/organizations': { title: '机构管理', subtitle: '管理组织架构与成员' }, '/organizations': { title: '机构管理', subtitle: '管理组织架构与成员' },
'/settings': { title: '系统设置', subtitle: '配置系统参数' }, '/settings': { title: '系统设置', subtitle: '配置系统参数' },
'/ai/chat': { title: 'AI 对话', subtitle: '智能助手' }, '/ai/chat': { title: 'AI 对话', subtitle: '智能助手' },
'/ai/models': { title: 'AI 模型', subtitle: '管理平台与模型配置' },
'/ai/keys': { title: 'API 密钥', subtitle: '管理 API 访问密钥' },
} }
export function MainLayout() { export function MainLayout() {

2
frontend/react-shadcn/pc/src/components/layout/Sidebar.tsx

@ -2,6 +2,7 @@ import { NavLink } from 'react-router-dom'
import { import {
LayoutDashboard, Users, LogOut, Settings, FolderOpen, LayoutDashboard, Users, LogOut, Settings, FolderOpen,
Shield, Menu as MenuIcon, Building2, User, ChevronDown, Shield, Menu as MenuIcon, Building2, User, ChevronDown,
Cpu, Key, MessageSquare,
} from 'lucide-react' } from 'lucide-react'
import type { LucideIcon } from 'lucide-react' import type { LucideIcon } from 'lucide-react'
import { useAuth } from '@/contexts/AuthContext' import { useAuth } from '@/contexts/AuthContext'
@ -11,6 +12,7 @@ import type { MenuItem } from '@/types'
const iconMap: Record<string, LucideIcon> = { const iconMap: Record<string, LucideIcon> = {
User, LayoutDashboard, Users, FolderOpen, Shield, User, LayoutDashboard, Users, FolderOpen, Shield,
Menu: MenuIcon, Building2, Settings, Menu: MenuIcon, Building2, Settings,
Cpu, Key, MessageSquare,
} }
function getIcon(iconName: string): LucideIcon { function getIcon(iconName: string): LucideIcon {

469
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<AIApiKeyInfo[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(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<AIApiKeyInfo | null>(null)
const [formData, setFormData] = useState<Partial<AIApiKeyCreateRequest>>({
providerId: undefined,
keyValue: '',
remark: '',
})
const [providers, setProviders] = useState<AIProviderInfo[]>([])
const [isLoadingProviders, setIsLoadingProviders] = useState(false)
// Delete confirmation
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
const [keyToDelete, setKeyToDelete] = useState<AIApiKeyInfo | null>(null)
const [isDeleting, setIsDeleting] = useState(false)
// Password visibility
const [visibleKeys, setVisibleKeys] = useState<Set<number>>(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 (
<div className="space-y-6 animate-fade-in">
{/* Header */}
<Card>
<CardContent className="p-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex-1 w-full sm:max-w-md">
<Input
placeholder="搜索密钥..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
leftIcon={<Search className="h-4 w-4" />}
/>
</div>
<Button onClick={() => openModal()} className="whitespace-nowrap">
<Plus className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
{/* Error Message */}
{error && (
<div className="p-4 bg-red-500/10 text-red-400 rounded-lg flex justify-between items-center">
<span>{error}</span>
<button onClick={fetchApiKeys} className="underline hover:text-red-300">
</button>
</div>
)}
{/* API Keys Table */}
<Card>
<CardHeader>
<CardTitle> ({filteredKeys.length})</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell>...</TableCell>
</TableRow>
) : filteredKeys.length === 0 ? (
<TableRow>
<TableCell></TableCell>
</TableRow>
) : (
filteredKeys.map((key) => (
<TableRow key={key.id}>
<TableCell className="font-medium text-foreground">
<div className="flex items-center gap-2">
<Key className="h-4 w-4 text-sky-400" />
{key.providerName}
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<span className="font-mono text-sm text-text-secondary">
{visibleKeys.has(key.id) ? key.keyValue : key.keyPreview}
</span>
<button
onClick={() => toggleKeyVisibility(key.id)}
className="text-text-muted hover:text-foreground transition-colors"
title={visibleKeys.has(key.id) ? '隐藏' : '显示'}
>
{visibleKeys.has(key.id) ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
</div>
</TableCell>
<TableCell>
{key.isActive ? (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-500/20 text-green-400 border border-green-500/30">
</span>
) : (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-500/20 text-text-secondary border border-gray-500/30">
</span>
)}
</TableCell>
<TableCell className="text-text-secondary max-w-xs truncate">
{key.remark || '-'}
</TableCell>
<TableCell className="text-text-secondary text-sm">
{formatDate(key.createdAt)}
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => openModal(key)}
title="编辑"
>
<Edit2 className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => openDeleteConfirm(key)}
className="text-red-400 hover:text-red-300"
title="删除"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
{/* Create / Edit Modal */}
<Modal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false)
resetForm()
}}
title={editingKey ? '编辑密钥' : '新建密钥'}
size="md"
footer={
<>
<Button
variant="outline"
onClick={() => {
setIsModalOpen(false)
resetForm()
}}
>
</Button>
<Button onClick={editingKey ? handleUpdateKey : handleCreateKey}>
{editingKey ? '保存' : '创建'}
</Button>
</>
}
>
<div className="space-y-4">
{!editingKey && (
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
<span className="text-red-400">*</span>
</label>
{isLoadingProviders ? (
<div className="text-text-muted text-sm">...</div>
) : (
<select
className="w-full px-3 py-2 bg-card border border-border-secondary rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-sky-500/50 focus:border-sky-500"
value={formData.providerId || ''}
onChange={(e) =>
setFormData({ ...formData, providerId: Number(e.target.value) })
}
>
<option value=""></option>
{providers.map((provider) => (
<option key={provider.id} value={provider.id}>
{provider.name}
</option>
))}
</select>
)}
</div>
)}
<Input
label={editingKey ? '密钥值' : '密钥值'}
placeholder={editingKey ? '留空则不更新' : '请输入 API 密钥'}
value={formData.keyValue}
onChange={(e) => setFormData({ ...formData, keyValue: e.target.value })}
type="password"
required={!editingKey}
/>
{editingKey && (
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-medium text-foreground cursor-pointer">
<input
type="checkbox"
checked={formData.isActive ?? editingKey.isActive}
onChange={(e) =>
setFormData({ ...formData, 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"
/>
<span></span>
</label>
</div>
)}
<Input
label="备注"
placeholder="请输入备注信息"
value={formData.remark}
onChange={(e) => setFormData({ ...formData, remark: e.target.value })}
/>
</div>
</Modal>
{/* Delete Confirmation Modal */}
<Modal
isOpen={deleteConfirmOpen}
onClose={() => {
setDeleteConfirmOpen(false)
setKeyToDelete(null)
}}
title="确认删除"
size="sm"
footer={
<>
<Button
variant="outline"
onClick={() => {
setDeleteConfirmOpen(false)
setKeyToDelete(null)
}}
disabled={isDeleting}
>
</Button>
<Button
variant="destructive"
onClick={handleDeleteKey}
disabled={isDeleting}
>
{isDeleting ? '删除中...' : '确认删除'}
</Button>
</>
}
>
<div className="py-4">
<p className="text-foreground">
{' '}
<span className="font-medium text-foreground">{keyToDelete?.providerName}</span>{' '}
</p>
<p className="text-sm text-text-muted mt-2"></p>
</div>
</Modal>
</div>
)
}

988
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<AIProviderInfo[]>([])
const [isLoadingProviders, setIsLoadingProviders] = useState(true)
const [providerError, setProviderError] = useState<string | null>(null)
const [providerSearchQuery, setProviderSearchQuery] = useState('')
const [providerPage, setProviderPage] = useState(1)
const [providerTotal, setProviderTotal] = useState(0)
const providerPageSize = 20
// Models state
const [models, setModels] = useState<AIModelInfo[]>([])
const [isLoadingModels, setIsLoadingModels] = useState(true)
const [modelError, setModelError] = useState<string | null>(null)
const [modelSearchQuery, setModelSearchQuery] = useState('')
// Provider Create/Edit modal
const [isProviderModalOpen, setIsProviderModalOpen] = useState(false)
const [editingProvider, setEditingProvider] = useState<AIProviderInfo | null>(null)
const [providerFormData, setProviderFormData] = useState<Partial<AIProviderCreateRequest & { isActive?: boolean }>>({
name: '',
displayName: '',
baseUrl: '',
sdkType: 'openai',
protocol: 'openai',
sortOrder: 0,
})
// Model Create/Edit modal
const [isModelModalOpen, setIsModelModalOpen] = useState(false)
const [editingModel, setEditingModel] = useState<AIModelInfo | null>(null)
const [modelFormData, setModelFormData] = useState<Partial<AIModelCreateRequest & { isActive?: boolean }>>({
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<AIProviderInfo | null>(null)
const [isDeletingProvider, setIsDeletingProvider] = useState(false)
const [deleteModelConfirmOpen, setDeleteModelConfirmOpen] = useState(false)
const [modelToDelete, setModelToDelete] = useState<AIModelInfo | null>(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 (
<div className="space-y-6 animate-fade-in">
{/* Tabs */}
<Card>
<CardContent className="p-6">
<div className="flex gap-2 border-b border-border-secondary">
<button
onClick={() => setActiveTab('providers')}
className={`px-4 py-2 -mb-px font-medium text-sm transition-colors ${
activeTab === 'providers'
? 'border-b-2 border-sky-400 text-sky-400'
: 'text-text-secondary hover:text-foreground'
}`}
>
<Server className="inline-block h-4 w-4 mr-1.5 -mt-0.5" />
</button>
<button
onClick={() => setActiveTab('models')}
className={`px-4 py-2 -mb-px font-medium text-sm transition-colors ${
activeTab === 'models'
? 'border-b-2 border-sky-400 text-sky-400'
: 'text-text-secondary hover:text-foreground'
}`}
>
<Cpu className="inline-block h-4 w-4 mr-1.5 -mt-0.5" />
</button>
</div>
</CardContent>
</Card>
{/* Providers Tab */}
{activeTab === 'providers' && (
<>
{/* Header */}
<Card>
<CardContent className="p-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex-1 w-full sm:max-w-md">
<Input
placeholder="搜索AI平台..."
value={providerSearchQuery}
onChange={(e) => setProviderSearchQuery(e.target.value)}
leftIcon={<Search className="h-4 w-4" />}
/>
</div>
<Button onClick={() => openProviderModal()} className="whitespace-nowrap">
<Plus className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
{/* Error Message */}
{providerError && (
<div className="p-4 bg-red-500/10 text-red-400 rounded-lg flex justify-between items-center">
<span>{providerError}</span>
<button onClick={fetchProviders} className="underline hover:text-red-300">
</button>
</div>
)}
{/* Providers Table */}
<Card>
<CardHeader>
<CardTitle> ({filteredProviders.length})</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>URL</TableHead>
<TableHead>SDK类型</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoadingProviders ? (
<TableRow>
<TableCell>...</TableCell>
</TableRow>
) : filteredProviders.length === 0 ? (
<TableRow>
<TableCell></TableCell>
</TableRow>
) : (
filteredProviders.map((provider) => (
<TableRow key={provider.id}>
<TableCell className="font-medium text-foreground">
{provider.name}
</TableCell>
<TableCell className="text-text-secondary">
{provider.displayName}
</TableCell>
<TableCell className="text-text-secondary font-mono text-sm max-w-xs truncate">
{provider.baseUrl}
</TableCell>
<TableCell>
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-sky-500/20 text-sky-400 border border-sky-500/30">
{provider.sdkType}
</span>
</TableCell>
<TableCell>
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-500/20 text-purple-400 border border-purple-500/30">
{provider.protocol}
</span>
</TableCell>
<TableCell>
{provider.isActive ? (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-500/20 text-green-400 border border-green-500/30">
</span>
) : (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-500/20 text-text-secondary border border-gray-500/30">
</span>
)}
</TableCell>
<TableCell className="text-text-secondary">
{provider.sortOrder}
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => openProviderModal(provider)}
title="编辑"
>
<Edit2 className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => openProviderDeleteConfirm(provider)}
className="text-red-400 hover:text-red-300"
title="删除"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</>
)}
{/* Models Tab */}
{activeTab === 'models' && (
<>
{/* Header */}
<Card>
<CardContent className="p-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex-1 w-full sm:max-w-md">
<Input
placeholder="搜索AI模型..."
value={modelSearchQuery}
onChange={(e) => setModelSearchQuery(e.target.value)}
leftIcon={<Search className="h-4 w-4" />}
/>
</div>
<Button onClick={() => openModelModal()} className="whitespace-nowrap">
<Plus className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
{/* Error Message */}
{modelError && (
<div className="p-4 bg-red-500/10 text-red-400 rounded-lg flex justify-between items-center">
<span>{modelError}</span>
<button onClick={fetchModels} className="underline hover:text-red-300">
</button>
</div>
)}
{/* Models Table */}
<Card>
<CardHeader>
<CardTitle> ({filteredModels.length})</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>Token</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoadingModels ? (
<TableRow>
<TableCell>...</TableCell>
</TableRow>
) : filteredModels.length === 0 ? (
<TableRow>
<TableCell></TableCell>
</TableRow>
) : (
filteredModels.map((model) => (
<TableRow key={model.id}>
<TableCell className="font-medium text-foreground font-mono text-sm">
{model.modelId}
</TableCell>
<TableCell className="text-text-secondary">
{model.displayName}
</TableCell>
<TableCell>
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-sky-500/20 text-sky-400 border border-sky-500/30">
{model.providerName}
</span>
</TableCell>
<TableCell className="text-text-secondary font-mono text-sm">
{formatPrice(model.inputPrice)}
</TableCell>
<TableCell className="text-text-secondary font-mono text-sm">
{formatPrice(model.outputPrice)}
</TableCell>
<TableCell className="text-text-secondary">
{model.maxTokens.toLocaleString()}
</TableCell>
<TableCell className="text-text-secondary">
{model.contextWindow.toLocaleString()}
</TableCell>
<TableCell>
{model.supportsStream ? (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-500/20 text-green-400 border border-green-500/30">
</span>
) : (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-500/20 text-text-secondary border border-gray-500/30">
-
</span>
)}
</TableCell>
<TableCell>
{model.supportsVision ? (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-500/20 text-green-400 border border-green-500/30">
</span>
) : (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-500/20 text-text-secondary border border-gray-500/30">
-
</span>
)}
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => openModelModal(model)}
title="编辑"
>
<Edit2 className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => openModelDeleteConfirm(model)}
className="text-red-400 hover:text-red-300"
title="删除"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</>
)}
{/* Provider Create / Edit Modal */}
<Modal
isOpen={isProviderModalOpen}
onClose={() => {
setIsProviderModalOpen(false)
resetProviderForm()
}}
title={editingProvider ? '编辑AI平台' : '新建AI平台'}
size="md"
footer={
<>
<Button
variant="outline"
onClick={() => {
setIsProviderModalOpen(false)
resetProviderForm()
}}
>
</Button>
<Button onClick={editingProvider ? handleUpdateProvider : handleCreateProvider}>
{editingProvider ? '保存' : '创建'}
</Button>
</>
}
>
<div className="space-y-4">
<Input
label="平台名称"
placeholder="请输入平台名称,如 openai"
value={providerFormData.name}
onChange={(e) => setProviderFormData({ ...providerFormData, name: e.target.value })}
disabled={!!editingProvider}
/>
<Input
label="显示名称"
placeholder="请输入显示名称,如 OpenAI"
value={providerFormData.displayName}
onChange={(e) =>
setProviderFormData({ ...providerFormData, displayName: e.target.value })
}
/>
<Input
label="基础URL"
placeholder="请输入基础URL,如 https://api.openai.com/v1"
value={providerFormData.baseUrl}
onChange={(e) => setProviderFormData({ ...providerFormData, baseUrl: e.target.value })}
/>
<div>
<label className="block text-sm font-medium text-foreground mb-1.5">SDK类型</label>
<select
value={providerFormData.sdkType}
onChange={(e) => setProviderFormData({ ...providerFormData, sdkType: e.target.value })}
className="w-full px-3 py-2 bg-card border border-border-secondary rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-sky-500/50"
>
<option value="openai">openai</option>
<option value="anthropic">anthropic</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1.5"></label>
<select
value={providerFormData.protocol}
onChange={(e) =>
setProviderFormData({ ...providerFormData, protocol: e.target.value })
}
className="w-full px-3 py-2 bg-card border border-border-secondary rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-sky-500/50"
>
<option value="openai">openai</option>
<option value="anthropic">anthropic</option>
</select>
</div>
<Input
label="排序"
type="number"
placeholder="请输入排序值"
value={providerFormData.sortOrder?.toString() || '0'}
onChange={(e) =>
setProviderFormData({ ...providerFormData, sortOrder: parseInt(e.target.value) || 0 })
}
/>
{editingProvider && (
<div className="flex items-center gap-2">
<input
type="checkbox"
id="provider-active"
checked={providerFormData.isActive || false}
onChange={(e) =>
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"
/>
<label htmlFor="provider-active" className="text-sm text-foreground cursor-pointer">
</label>
</div>
)}
</div>
</Modal>
{/* Model Create / Edit Modal */}
<Modal
isOpen={isModelModalOpen}
onClose={() => {
setIsModelModalOpen(false)
resetModelForm()
}}
title={editingModel ? '编辑AI模型' : '新建AI模型'}
size="md"
footer={
<>
<Button
variant="outline"
onClick={() => {
setIsModelModalOpen(false)
resetModelForm()
}}
>
</Button>
<Button onClick={editingModel ? handleUpdateModel : handleCreateModel}>
{editingModel ? '保存' : '创建'}
</Button>
</>
}
>
<div className="space-y-4">
{!editingModel && (
<div>
<label className="block text-sm font-medium text-foreground mb-1.5"></label>
<select
value={modelFormData.providerId}
onChange={(e) =>
setModelFormData({ ...modelFormData, providerId: parseInt(e.target.value) })
}
className="w-full px-3 py-2 bg-card border border-border-secondary rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-sky-500/50"
>
{providers.map((provider) => (
<option key={provider.id} value={provider.id}>
{provider.displayName}
</option>
))}
</select>
</div>
)}
<Input
label="模型ID"
placeholder="请输入模型ID,如 gpt-4"
value={modelFormData.modelId}
onChange={(e) => setModelFormData({ ...modelFormData, modelId: e.target.value })}
disabled={!!editingModel}
/>
<Input
label="显示名称"
placeholder="请输入显示名称,如 GPT-4"
value={modelFormData.displayName}
onChange={(e) => setModelFormData({ ...modelFormData, displayName: e.target.value })}
/>
<Input
label="输入价格 ($/1K tokens)"
type="number"
step="0.01"
placeholder="请输入输入价格"
value={modelFormData.inputPrice?.toString() || '0'}
onChange={(e) =>
setModelFormData({ ...modelFormData, inputPrice: parseFloat(e.target.value) || 0 })
}
/>
<Input
label="输出价格 ($/1K tokens)"
type="number"
step="0.01"
placeholder="请输入输出价格"
value={modelFormData.outputPrice?.toString() || '0'}
onChange={(e) =>
setModelFormData({ ...modelFormData, outputPrice: parseFloat(e.target.value) || 0 })
}
/>
<Input
label="最大Token"
type="number"
placeholder="请输入最大Token数"
value={modelFormData.maxTokens?.toString() || '0'}
onChange={(e) =>
setModelFormData({ ...modelFormData, maxTokens: parseInt(e.target.value) || 0 })
}
/>
<Input
label="上下文窗口"
type="number"
placeholder="请输入上下文窗口大小"
value={modelFormData.contextWindow?.toString() || '0'}
onChange={(e) =>
setModelFormData({
...modelFormData,
contextWindow: parseInt(e.target.value) || 0,
})
}
/>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="model-stream"
checked={modelFormData.supportsStream || false}
onChange={(e) =>
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"
/>
<label htmlFor="model-stream" className="text-sm text-foreground cursor-pointer">
</label>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="model-vision"
checked={modelFormData.supportsVision || false}
onChange={(e) =>
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"
/>
<label htmlFor="model-vision" className="text-sm text-foreground cursor-pointer">
</label>
</div>
{editingModel && (
<div className="flex items-center gap-2">
<input
type="checkbox"
id="model-active"
checked={modelFormData.isActive || false}
onChange={(e) =>
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"
/>
<label htmlFor="model-active" className="text-sm text-foreground cursor-pointer">
</label>
</div>
)}
</div>
</Modal>
{/* Provider Delete Confirmation Modal */}
<Modal
isOpen={deleteProviderConfirmOpen}
onClose={() => {
setDeleteProviderConfirmOpen(false)
setProviderToDelete(null)
}}
title="确认删除"
size="sm"
footer={
<>
<Button
variant="outline"
onClick={() => {
setDeleteProviderConfirmOpen(false)
setProviderToDelete(null)
}}
disabled={isDeletingProvider}
>
</Button>
<Button
variant="destructive"
onClick={handleDeleteProvider}
disabled={isDeletingProvider}
>
{isDeletingProvider ? '删除中...' : '确认删除'}
</Button>
</>
}
>
<div className="py-4">
<p className="text-foreground">
{' '}
<span className="font-medium text-foreground">{providerToDelete?.displayName}</span>{' '}
</p>
<p className="text-sm text-text-muted mt-2"></p>
</div>
</Modal>
{/* Model Delete Confirmation Modal */}
<Modal
isOpen={deleteModelConfirmOpen}
onClose={() => {
setDeleteModelConfirmOpen(false)
setModelToDelete(null)
}}
title="确认删除"
size="sm"
footer={
<>
<Button
variant="outline"
onClick={() => {
setDeleteModelConfirmOpen(false)
setModelToDelete(null)
}}
disabled={isDeletingModel}
>
</Button>
<Button variant="destructive" onClick={handleDeleteModel} disabled={isDeletingModel}>
{isDeletingModel ? '删除中...' : '确认删除'}
</Button>
</>
}
>
<div className="py-4">
<p className="text-foreground">
{' '}
<span className="font-medium text-foreground">{modelToDelete?.displayName}</span>
</p>
<p className="text-sm text-text-muted mt-2"></p>
</div>
</Modal>
</div>
)
}

55
frontend/react-shadcn/pc/src/services/api.ts

@ -32,6 +32,14 @@ import type {
AIChatMessage, AIChatMessage,
AIQuotaInfo, AIQuotaInfo,
AIChatCompletionRequest, AIChatCompletionRequest,
AIProviderInfo,
AIProviderCreateRequest,
AIProviderUpdateRequest,
AIModelCreateRequest,
AIModelUpdateRequest,
AIApiKeyInfo,
AIApiKeyCreateRequest,
AIApiKeyUpdateRequest,
} from '@/types' } from '@/types'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8888/api/v1' 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<AIQuotaInfo>('/ai/quota/me') return this.request<AIQuotaInfo>('/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<AIProviderInfo> {
return this.request<AIProviderInfo>('/ai/provider', { method: 'POST', body: JSON.stringify(data) })
}
async updateAIProvider(id: number, data: AIProviderUpdateRequest): Promise<AIProviderInfo> {
return this.request<AIProviderInfo>(`/ai/provider/${id}`, { method: 'PUT', body: JSON.stringify(data) })
}
async deleteAIProvider(id: number): Promise<void> {
await this.request<void>(`/ai/provider/${id}`, { method: 'DELETE' })
}
// AI Models (admin)
async createAIModel(data: AIModelCreateRequest): Promise<AIModelInfo> {
return this.request<AIModelInfo>('/ai/model', { method: 'POST', body: JSON.stringify(data) })
}
async updateAIModel(id: number, data: AIModelUpdateRequest): Promise<AIModelInfo> {
return this.request<AIModelInfo>(`/ai/model/${id}`, { method: 'PUT', body: JSON.stringify(data) })
}
async deleteAIModel(id: number): Promise<void> {
await this.request<void>(`/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<AIApiKeyInfo> {
return this.request<AIApiKeyInfo>('/ai/key', { method: 'POST', body: JSON.stringify(data) })
}
async updateAIApiKey(id: number, data: AIApiKeyUpdateRequest): Promise<AIApiKeyInfo> {
return this.request<AIApiKeyInfo>(`/ai/key/${id}`, { method: 'PUT', body: JSON.stringify(data) })
}
async deleteAIApiKey(id: number): Promise<void> {
await this.request<void>(`/ai/key/${id}`, { method: 'DELETE' })
}
// Health check // Health check
async healthCheck(): Promise<{ status: string }> { async healthCheck(): Promise<{ status: string }> {
try { try {

80
frontend/react-shadcn/pc/src/types/index.ts

@ -339,3 +339,83 @@ export interface AIChatCompletionRequest {
temperature?: number temperature?: number
conversation_id?: 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
}

Loading…
Cancel
Save