Browse Source
- ai.api: chat completions, conversations CRUD, models, quota - 8 handlers + 8 logic stubs generated - Routes registered with Cors,Log,Auth middlewaremaster
20 changed files with 862 additions and 0 deletions
@ -0,0 +1,133 @@ |
|||
syntax = "v1" |
|||
|
|||
// ========== AI Chat Types ========== |
|||
|
|||
type ( |
|||
AIChatMessage { |
|||
Role string `json:"role"` |
|||
Content string `json:"content"` |
|||
} |
|||
|
|||
AIChatCompletionRequest { |
|||
Model string `json:"model"` |
|||
Messages []AIChatMessage `json:"messages"` |
|||
Stream bool `json:"stream,optional"` |
|||
MaxTokens int `json:"max_tokens,optional"` |
|||
Temperature float64 `json:"temperature,optional"` |
|||
ConversationId int64 `json:"conversation_id,optional,string"` |
|||
} |
|||
|
|||
AIChatCompletionChoice { |
|||
Index int `json:"index"` |
|||
FinishReason string `json:"finish_reason"` |
|||
Message AIChatMessage `json:"message"` |
|||
} |
|||
|
|||
AIChatCompletionUsage { |
|||
PromptTokens int `json:"prompt_tokens"` |
|||
CompletionTokens int `json:"completion_tokens"` |
|||
TotalTokens int `json:"total_tokens"` |
|||
} |
|||
|
|||
AIChatCompletionResponse { |
|||
Id string `json:"id"` |
|||
Object string `json:"object"` |
|||
Model string `json:"model"` |
|||
Choices []AIChatCompletionChoice `json:"choices"` |
|||
Usage AIChatCompletionUsage `json:"usage"` |
|||
} |
|||
) |
|||
|
|||
// ========== Conversation Types ========== |
|||
|
|||
type ( |
|||
AIConversationInfo { |
|||
Id int64 `json:"id,string"` |
|||
Title string `json:"title"` |
|||
ModelId string `json:"modelId"` |
|||
ProviderId int64 `json:"providerId,string"` |
|||
TotalTokens int64 `json:"totalTokens"` |
|||
TotalCost float64 `json:"totalCost"` |
|||
IsArchived bool `json:"isArchived"` |
|||
CreatedAt string `json:"createdAt"` |
|||
UpdatedAt string `json:"updatedAt"` |
|||
} |
|||
|
|||
AIMessageInfo { |
|||
Id int64 `json:"id,string"` |
|||
ConversationId int64 `json:"conversationId,string"` |
|||
Role string `json:"role"` |
|||
Content string `json:"content"` |
|||
TokenCount int `json:"tokenCount"` |
|||
Cost float64 `json:"cost"` |
|||
ModelId string `json:"modelId"` |
|||
LatencyMs int `json:"latencyMs"` |
|||
CreatedAt string `json:"createdAt"` |
|||
} |
|||
|
|||
AIConversationListRequest { |
|||
Page int64 `form:"page,optional,default=1"` |
|||
PageSize int64 `form:"pageSize,optional,default=20"` |
|||
} |
|||
|
|||
AIConversationListResponse { |
|||
List []AIConversationInfo `json:"list"` |
|||
Total int64 `json:"total"` |
|||
} |
|||
|
|||
AIConversationCreateRequest { |
|||
Title string `json:"title,optional"` |
|||
ModelId string `json:"modelId,optional"` |
|||
} |
|||
|
|||
AIConversationGetRequest { |
|||
Id int64 `path:"id"` |
|||
} |
|||
|
|||
AIConversationDetailResponse { |
|||
Conversation AIConversationInfo `json:"conversation"` |
|||
Messages []AIMessageInfo `json:"messages"` |
|||
} |
|||
|
|||
AIConversationUpdateRequest { |
|||
Id int64 `path:"id"` |
|||
Title string `json:"title"` |
|||
} |
|||
|
|||
AIConversationDeleteRequest { |
|||
Id int64 `path:"id"` |
|||
} |
|||
) |
|||
|
|||
// ========== Model Types ========== |
|||
|
|||
type ( |
|||
AIModelInfo { |
|||
Id int64 `json:"id,string"` |
|||
ProviderId int64 `json:"providerId,string"` |
|||
ProviderName string `json:"providerName"` |
|||
ModelId string `json:"modelId"` |
|||
DisplayName string `json:"displayName"` |
|||
InputPrice float64 `json:"inputPrice"` |
|||
OutputPrice float64 `json:"outputPrice"` |
|||
MaxTokens int `json:"maxTokens"` |
|||
ContextWindow int `json:"contextWindow"` |
|||
SupportsStream bool `json:"supportsStream"` |
|||
SupportsVision bool `json:"supportsVision"` |
|||
} |
|||
|
|||
AIModelListResponse { |
|||
List []AIModelInfo `json:"list"` |
|||
} |
|||
) |
|||
|
|||
// ========== Quota Types ========== |
|||
|
|||
type ( |
|||
AIQuotaInfo { |
|||
Balance float64 `json:"balance"` |
|||
TotalRecharged float64 `json:"totalRecharged"` |
|||
TotalConsumed float64 `json:"totalConsumed"` |
|||
FrozenAmount float64 `json:"frozenAmount"` |
|||
} |
|||
) |
|||
@ -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 AiChatCompletionsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { |
|||
return func(w http.ResponseWriter, r *http.Request) { |
|||
var req types.AIChatCompletionRequest |
|||
if err := httpx.Parse(r, &req); err != nil { |
|||
httpx.ErrorCtx(r.Context(), w, err) |
|||
return |
|||
} |
|||
|
|||
l := ai.NewAiChatCompletionsLogic(r.Context(), svcCtx) |
|||
err := l.AiChatCompletions(&req) |
|||
if err != nil { |
|||
httpx.ErrorCtx(r.Context(), w, err) |
|||
} else { |
|||
httpx.Ok(w) |
|||
} |
|||
} |
|||
} |
|||
@ -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" |
|||
) |
|||
|
|||
// 创建对话
|
|||
func AiConversationCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { |
|||
return func(w http.ResponseWriter, r *http.Request) { |
|||
var req types.AIConversationCreateRequest |
|||
if err := httpx.Parse(r, &req); err != nil { |
|||
httpx.ErrorCtx(r.Context(), w, err) |
|||
return |
|||
} |
|||
|
|||
l := ai.NewAiConversationCreateLogic(r.Context(), svcCtx) |
|||
resp, err := l.AiConversationCreate(&req) |
|||
if err != nil { |
|||
httpx.ErrorCtx(r.Context(), w, err) |
|||
} else { |
|||
httpx.OkJsonCtx(r.Context(), w, resp) |
|||
} |
|||
} |
|||
} |
|||
@ -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" |
|||
) |
|||
|
|||
// 删除对话
|
|||
func AiConversationDeleteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { |
|||
return func(w http.ResponseWriter, r *http.Request) { |
|||
var req types.AIConversationDeleteRequest |
|||
if err := httpx.Parse(r, &req); err != nil { |
|||
httpx.ErrorCtx(r.Context(), w, err) |
|||
return |
|||
} |
|||
|
|||
l := ai.NewAiConversationDeleteLogic(r.Context(), svcCtx) |
|||
resp, err := l.AiConversationDelete(&req) |
|||
if err != nil { |
|||
httpx.ErrorCtx(r.Context(), w, err) |
|||
} else { |
|||
httpx.OkJsonCtx(r.Context(), w, resp) |
|||
} |
|||
} |
|||
} |
|||
@ -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" |
|||
) |
|||
|
|||
// 获取对话详情
|
|||
func AiConversationGetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { |
|||
return func(w http.ResponseWriter, r *http.Request) { |
|||
var req types.AIConversationGetRequest |
|||
if err := httpx.Parse(r, &req); err != nil { |
|||
httpx.ErrorCtx(r.Context(), w, err) |
|||
return |
|||
} |
|||
|
|||
l := ai.NewAiConversationGetLogic(r.Context(), svcCtx) |
|||
resp, err := l.AiConversationGet(&req) |
|||
if err != nil { |
|||
httpx.ErrorCtx(r.Context(), w, err) |
|||
} else { |
|||
httpx.OkJsonCtx(r.Context(), w, resp) |
|||
} |
|||
} |
|||
} |
|||
@ -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" |
|||
) |
|||
|
|||
// 获取对话列表
|
|||
func AiConversationListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { |
|||
return func(w http.ResponseWriter, r *http.Request) { |
|||
var req types.AIConversationListRequest |
|||
if err := httpx.Parse(r, &req); err != nil { |
|||
httpx.ErrorCtx(r.Context(), w, err) |
|||
return |
|||
} |
|||
|
|||
l := ai.NewAiConversationListLogic(r.Context(), svcCtx) |
|||
resp, err := l.AiConversationList(&req) |
|||
if err != nil { |
|||
httpx.ErrorCtx(r.Context(), w, err) |
|||
} else { |
|||
httpx.OkJsonCtx(r.Context(), w, resp) |
|||
} |
|||
} |
|||
} |
|||
@ -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" |
|||
) |
|||
|
|||
// 更新对话
|
|||
func AiConversationUpdateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { |
|||
return func(w http.ResponseWriter, r *http.Request) { |
|||
var req types.AIConversationUpdateRequest |
|||
if err := httpx.Parse(r, &req); err != nil { |
|||
httpx.ErrorCtx(r.Context(), w, err) |
|||
return |
|||
} |
|||
|
|||
l := ai.NewAiConversationUpdateLogic(r.Context(), svcCtx) |
|||
resp, err := l.AiConversationUpdate(&req) |
|||
if err != nil { |
|||
httpx.ErrorCtx(r.Context(), w, err) |
|||
} else { |
|||
httpx.OkJsonCtx(r.Context(), w, resp) |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// 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/zeromicro/go-zero/rest/httpx" |
|||
) |
|||
|
|||
// 获取模型列表
|
|||
func AiModelListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { |
|||
return func(w http.ResponseWriter, r *http.Request) { |
|||
l := ai.NewAiModelListLogic(r.Context(), svcCtx) |
|||
resp, err := l.AiModelList() |
|||
if err != nil { |
|||
httpx.ErrorCtx(r.Context(), w, err) |
|||
} else { |
|||
httpx.OkJsonCtx(r.Context(), w, resp) |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// 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/zeromicro/go-zero/rest/httpx" |
|||
) |
|||
|
|||
// 获取我的配额
|
|||
func AiQuotaMeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { |
|||
return func(w http.ResponseWriter, r *http.Request) { |
|||
l := ai.NewAiQuotaMeLogic(r.Context(), svcCtx) |
|||
resp, err := l.AiQuotaMe() |
|||
if err != nil { |
|||
httpx.ErrorCtx(r.Context(), w, err) |
|||
} else { |
|||
httpx.OkJsonCtx(r.Context(), w, resp) |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// 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/zeromicro/go-zero/core/logx" |
|||
) |
|||
|
|||
type AiChatCompletionsLogic struct { |
|||
logx.Logger |
|||
ctx context.Context |
|||
svcCtx *svc.ServiceContext |
|||
} |
|||
|
|||
// AI 对话补全
|
|||
func NewAiChatCompletionsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiChatCompletionsLogic { |
|||
return &AiChatCompletionsLogic{ |
|||
Logger: logx.WithContext(ctx), |
|||
ctx: ctx, |
|||
svcCtx: svcCtx, |
|||
} |
|||
} |
|||
|
|||
func (l *AiChatCompletionsLogic) AiChatCompletions(req *types.AIChatCompletionRequest) error { |
|||
// todo: add your logic here and delete this line
|
|||
|
|||
return nil |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// 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/zeromicro/go-zero/core/logx" |
|||
) |
|||
|
|||
type AiConversationCreateLogic struct { |
|||
logx.Logger |
|||
ctx context.Context |
|||
svcCtx *svc.ServiceContext |
|||
} |
|||
|
|||
// 创建对话
|
|||
func NewAiConversationCreateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiConversationCreateLogic { |
|||
return &AiConversationCreateLogic{ |
|||
Logger: logx.WithContext(ctx), |
|||
ctx: ctx, |
|||
svcCtx: svcCtx, |
|||
} |
|||
} |
|||
|
|||
func (l *AiConversationCreateLogic) AiConversationCreate(req *types.AIConversationCreateRequest) (resp *types.AIConversationInfo, err error) { |
|||
// todo: add your logic here and delete this line
|
|||
|
|||
return |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// 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/zeromicro/go-zero/core/logx" |
|||
) |
|||
|
|||
type AiConversationDeleteLogic struct { |
|||
logx.Logger |
|||
ctx context.Context |
|||
svcCtx *svc.ServiceContext |
|||
} |
|||
|
|||
// 删除对话
|
|||
func NewAiConversationDeleteLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiConversationDeleteLogic { |
|||
return &AiConversationDeleteLogic{ |
|||
Logger: logx.WithContext(ctx), |
|||
ctx: ctx, |
|||
svcCtx: svcCtx, |
|||
} |
|||
} |
|||
|
|||
func (l *AiConversationDeleteLogic) AiConversationDelete(req *types.AIConversationDeleteRequest) (resp *types.Response, err error) { |
|||
// todo: add your logic here and delete this line
|
|||
|
|||
return |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// 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/zeromicro/go-zero/core/logx" |
|||
) |
|||
|
|||
type AiConversationGetLogic struct { |
|||
logx.Logger |
|||
ctx context.Context |
|||
svcCtx *svc.ServiceContext |
|||
} |
|||
|
|||
// 获取对话详情
|
|||
func NewAiConversationGetLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiConversationGetLogic { |
|||
return &AiConversationGetLogic{ |
|||
Logger: logx.WithContext(ctx), |
|||
ctx: ctx, |
|||
svcCtx: svcCtx, |
|||
} |
|||
} |
|||
|
|||
func (l *AiConversationGetLogic) AiConversationGet(req *types.AIConversationGetRequest) (resp *types.AIConversationDetailResponse, err error) { |
|||
// todo: add your logic here and delete this line
|
|||
|
|||
return |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// 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/zeromicro/go-zero/core/logx" |
|||
) |
|||
|
|||
type AiConversationListLogic struct { |
|||
logx.Logger |
|||
ctx context.Context |
|||
svcCtx *svc.ServiceContext |
|||
} |
|||
|
|||
// 获取对话列表
|
|||
func NewAiConversationListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiConversationListLogic { |
|||
return &AiConversationListLogic{ |
|||
Logger: logx.WithContext(ctx), |
|||
ctx: ctx, |
|||
svcCtx: svcCtx, |
|||
} |
|||
} |
|||
|
|||
func (l *AiConversationListLogic) AiConversationList(req *types.AIConversationListRequest) (resp *types.AIConversationListResponse, err error) { |
|||
// todo: add your logic here and delete this line
|
|||
|
|||
return |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// 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/zeromicro/go-zero/core/logx" |
|||
) |
|||
|
|||
type AiConversationUpdateLogic struct { |
|||
logx.Logger |
|||
ctx context.Context |
|||
svcCtx *svc.ServiceContext |
|||
} |
|||
|
|||
// 更新对话
|
|||
func NewAiConversationUpdateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiConversationUpdateLogic { |
|||
return &AiConversationUpdateLogic{ |
|||
Logger: logx.WithContext(ctx), |
|||
ctx: ctx, |
|||
svcCtx: svcCtx, |
|||
} |
|||
} |
|||
|
|||
func (l *AiConversationUpdateLogic) AiConversationUpdate(req *types.AIConversationUpdateRequest) (resp *types.AIConversationInfo, err error) { |
|||
// todo: add your logic here and delete this line
|
|||
|
|||
return |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// 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/zeromicro/go-zero/core/logx" |
|||
) |
|||
|
|||
type AiModelListLogic struct { |
|||
logx.Logger |
|||
ctx context.Context |
|||
svcCtx *svc.ServiceContext |
|||
} |
|||
|
|||
// 获取模型列表
|
|||
func NewAiModelListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiModelListLogic { |
|||
return &AiModelListLogic{ |
|||
Logger: logx.WithContext(ctx), |
|||
ctx: ctx, |
|||
svcCtx: svcCtx, |
|||
} |
|||
} |
|||
|
|||
func (l *AiModelListLogic) AiModelList() (resp *types.AIModelListResponse, err error) { |
|||
// todo: add your logic here and delete this line
|
|||
|
|||
return |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// 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/zeromicro/go-zero/core/logx" |
|||
) |
|||
|
|||
type AiQuotaMeLogic struct { |
|||
logx.Logger |
|||
ctx context.Context |
|||
svcCtx *svc.ServiceContext |
|||
} |
|||
|
|||
// 获取我的配额
|
|||
func NewAiQuotaMeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AiQuotaMeLogic { |
|||
return &AiQuotaMeLogic{ |
|||
Logger: logx.WithContext(ctx), |
|||
ctx: ctx, |
|||
svcCtx: svcCtx, |
|||
} |
|||
} |
|||
|
|||
func (l *AiQuotaMeLogic) AiQuotaMe() (resp *types.AIQuotaInfo, err error) { |
|||
// todo: add your logic here and delete this line
|
|||
|
|||
return |
|||
} |
|||
Loading…
Reference in new issue