diff --git a/data/config.json b/data/config.json index bac82a1..6895a07 100644 --- a/data/config.json +++ b/data/config.json @@ -41,6 +41,10 @@ "apiUrl": "https://api-proxy.oaipro.com/v1/moderations", "message": "您的消息包含不当内容,请修改后重试!" }, + "oneapi": { + "token": "", + "domain": "" + }, "log": { "level": "info", "encoding": "console", diff --git a/frontend/src/api/services/accountService.ts b/frontend/src/api/services/accountService.ts index 38c8fde..c7de295 100644 --- a/frontend/src/api/services/accountService.ts +++ b/frontend/src/api/services/accountService.ts @@ -11,6 +11,7 @@ export enum AccountApi { search = '/account/search', shareAccounts = '/share_accounts', loginFree = '/login_free_account', + getOneApiChannel = '/account/oneapi/channels', } const getAccountList = () => @@ -43,6 +44,7 @@ export interface AccountAddReq { sessionKey?: string; refreshToken?: string; accessToken?: string; + oneApiChannelId?: number; } interface ShareAccountListResp { @@ -57,14 +59,23 @@ interface LoginFreeAccountResp { SelectType?: string, } +interface OneApiChannel { + id: number; + name: string; + group: string; + status: number; +} + const addAccount = (data: AccountAddReq) => apiClient.post({ url: AccountApi.add, data }); const updateAccount = (data: AccountAddReq) => apiClient.post({ url: AccountApi.update, data }); const deleteAccount = (id: number) => apiClient.post({ url: AccountApi.delete, data: { id } }); const refreshAccount = (id: number) => apiClient.post({ url: AccountApi.refresh, data: { id } }); const getShareAccountList = () => apiClient.post({ url: AccountApi.shareAccounts }); +const getOneApiChannelList = () => apiClient.post({ url: AccountApi.getOneApiChannel }); const loginFreeAccount = (data: LoginFreeAccountResp) => apiClient.post({ url: AccountApi.loginFree, data }); export default { + getOneApiChannelList, getAccountList, searchAccountList, addAccount, diff --git a/frontend/src/pages/token/account/chatgpt.tsx b/frontend/src/pages/token/account/chatgpt.tsx index 91126eb..5442e20 100644 --- a/frontend/src/pages/token/account/chatgpt.tsx +++ b/frontend/src/pages/token/account/chatgpt.tsx @@ -73,6 +73,7 @@ export default function AccountPage() { refreshToken: '', accessToken: '', shared: 0, + oneApiChannelId: undefined, } const [accountModalProps, setAccountModalProps] = useState({ @@ -81,7 +82,6 @@ export default function AccountPage() { show: false, onOk: (values: AccountAddReq, callback) => { if (values.id) { - console.log(values) updateAccountMutation.mutate(values, { onSuccess: () => { setAccountModalProps((prev) => ({ ...prev, show: false })); @@ -338,6 +338,7 @@ export default function AccountPage() { shared: record.shared, refreshToken: record.refreshToken, accessToken: record.accessToken, + oneApiChannelId: record.oneApiChannelId, }, })); }; diff --git a/frontend/src/pages/token/account/components/AccountModal.tsx b/frontend/src/pages/token/account/components/AccountModal.tsx index bb4b146..414df88 100644 --- a/frontend/src/pages/token/account/components/AccountModal.tsx +++ b/frontend/src/pages/token/account/components/AccountModal.tsx @@ -1,9 +1,22 @@ -import {AccountAddReq} from "@/api/services/accountService.ts"; -import {Button, Form, Input, Modal, Space, Switch, Tooltip} from "antd"; +import accountService, {AccountAddReq} from "@/api/services/accountService.ts"; +import { + Button, + Form, + Input, + Modal, + Select, + Space, + Spin, + Switch, + Tag, + Tooltip, + Typography +} from "antd"; import {useEffect, useState} from "react"; import {useTranslation} from "react-i18next"; import Password from "antd/es/input/Password"; import {InfoCircleOutlined} from "@ant-design/icons"; +import {useQuery} from "@tanstack/react-query"; export type AccountModalProps = { formValue: AccountAddReq; @@ -18,6 +31,13 @@ export function AccountModal({ title, show, formValue, onOk, onCancel }: Account const [loading, setLoading] = useState(false); const { t } = useTranslation(); + const { data, isLoading } = useQuery({ + queryKey: ['accounts', 'one-api-channel', formValue], + queryFn: () => accountService.getOneApiChannelList(), + enabled: show, + }); + + useEffect(() => { if (show) { form1.setFieldsValue(formValue) @@ -96,6 +116,38 @@ export function AccountModal({ title, show, formValue, onOk, onCancel }: Account > + {formValue.accountType === 'chatgpt' ? ( + + label={"OneAPI 渠道"} + name={"oneApiChannelId"} + > + + + ): null} {formValue.accountType === 'chatgpt' ? ( <> { - console.log(value); - switch (value) { - case -1: + console.log(value, typeof value); + switch (`${value}`) { + case '-1': return '无限制'; - case 0: + case '0': return '禁用'; default: - return `${value}`; + return value !== undefined ? String(value) : ''; } - } + }; + const limitParser = (value: string | undefined) => { switch (value) { @@ -126,6 +127,7 @@ export function ShareModal({ title, show, formValue, onOk, onCancel }: ShareModa style={{ width: '100%' }} formatter={limitFormatter} parser={limitParser} + min={-1} /> @@ -135,6 +137,7 @@ export function ShareModal({ title, show, formValue, onOk, onCancel }: ShareModa style={{ width: '100%' }} formatter={limitFormatter} parser={limitParser} + min={-1} /> @@ -144,6 +147,7 @@ export function ShareModal({ title, show, formValue, onOk, onCancel }: ShareModa style={{ width: '100%' }} formatter={limitFormatter} parser={limitParser} + min={-1} /> @@ -153,6 +157,7 @@ export function ShareModal({ title, show, formValue, onOk, onCancel }: ShareModa style={{ width: '100%' }} formatter={limitFormatter} parser={limitParser} + min={-1} /> @@ -162,6 +167,7 @@ export function ShareModal({ title, show, formValue, onOk, onCancel }: ShareModa style={{ width: '100%' }} formatter={limitFormatter} parser={limitParser} + min={-1} /> diff --git a/frontend/types/entity.ts b/frontend/types/entity.ts index 5dc4654..e4c0bec 100644 --- a/frontend/types/entity.ts +++ b/frontend/types/entity.ts @@ -67,6 +67,7 @@ export interface Account { export interface ChatGPTAccount extends Account { refreshToken?: string; accessToken?: string; + oneApiChannelId?: number; } export interface ClaudeAccount extends Account { diff --git a/internal/handler/account.go b/internal/handler/account.go index f18a17b..7a977e4 100644 --- a/internal/handler/account.go +++ b/internal/handler/account.go @@ -135,3 +135,12 @@ func (h *AccountHandler) LoginShareAccount(ctx *gin.Context) { } v1.HandleSuccess(ctx, url) } + +func (h *AccountHandler) GetOneApiChannelList(ctx *gin.Context) { + channels, err := h.accountService.GetOneApiChannelList(ctx) + if err != nil { + v1.HandleError(ctx, http.StatusInternalServerError, err, nil) + return + } + v1.HandleSuccess(ctx, channels) +} diff --git a/internal/model/account.go b/internal/model/account.go index 2646c56..b87dd83 100644 --- a/internal/model/account.go +++ b/internal/model/account.go @@ -1,18 +1,19 @@ package model type Account struct { - ID uint `json:"id" gorm:"primaryKey" gorm:"column:id"` - Email string `json:"email" gorm:"column:email"` - Password string `json:"password" gorm:"column:password"` - SessionToken string `json:"sessionToken" gorm:"column:session_token"` - AccessToken string `json:"accessToken" gorm:"column:access_token"` - CreateTime *LocalTime `json:"createTime" gorm:"autoCreateTime" gorm:"column:create_time"` - UpdateTime *LocalTime `json:"updateTime" gorm:"autoUpdateTime" gorm:"column:update_time"` - Shared int `json:"shared" gorm:"column:shared"` - RefreshToken string `json:"refreshToken" gorm:"column:refresh_token"` - AccountType string `json:"accountType" gorm:"column:account_type" gorm:"default:chatgpt"` - SessionKey string `json:"sessionKey" gorm:"column:session_key"` - Shares []Share `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE"` // 外键关系 + ID uint `json:"id" gorm:"primaryKey" gorm:"column:id"` + Email string `json:"email" gorm:"column:email"` + Password string `json:"password" gorm:"column:password"` + SessionToken string `json:"sessionToken" gorm:"column:session_token"` + AccessToken string `json:"accessToken" gorm:"column:access_token"` + CreateTime *LocalTime `json:"createTime" gorm:"autoCreateTime" gorm:"column:create_time"` + UpdateTime *LocalTime `json:"updateTime" gorm:"autoUpdateTime" gorm:"column:update_time"` + Shared int `json:"shared" gorm:"column:shared"` + RefreshToken string `json:"refreshToken" gorm:"column:refresh_token"` + AccountType string `json:"accountType" gorm:"column:account_type" gorm:"default:chatgpt"` + SessionKey string `json:"sessionKey" gorm:"column:session_key"` + OneApiChannelId string `json:"oneApiChannelId" gorm:"column:one_api_channel_id;default:''"` + Shares []Share `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE"` // 外键关系 } func (m *Account) TableName() string { diff --git a/internal/model/oneApiChannel.go b/internal/model/oneApiChannel.go new file mode 100644 index 0000000..dc7dd02 --- /dev/null +++ b/internal/model/oneApiChannel.go @@ -0,0 +1,27 @@ +package model + +type OneApiChannel struct { + Id int `json:"id"` + Type int `json:"type" gorm:"default:0"` + Key string `json:"key" gorm:"not null"` + OpenAIOrganization string `json:"openai_organization"` + TestModel string `json:"test_model"` + Status int `json:"status" gorm:"default:1"` + Name string `json:"name" gorm:"index"` + Weight uint `json:"weight" gorm:"default:0"` + CreatedTime int64 `json:"created_time" gorm:"bigint"` + TestTime int64 `json:"test_time" gorm:"bigint"` + ResponseTime int `json:"response_time"` // in milliseconds + BaseURL string `json:"base_url" gorm:"column:base_url;default:''"` + Other string `json:"other"` + Balance float64 `json:"balance"` // in USD + BalanceUpdatedTime int64 `json:"balance_updated_time" gorm:"bigint"` + Models string `json:"models"` + Group string `json:"group" gorm:"type:varchar(64);default:'default'"` + UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"` + ModelMapping string `json:"model_mapping" gorm:"type:varchar(1024);default:''"` + StatusCodeMapping string `json:"status_code_mapping" gorm:"type:varchar(1024);default:''"` + Priority int64 `json:"priority" gorm:"bigint;default:0"` + AutoBan int `json:"auto_ban" gorm:"default:1"` + OtherInfo string `json:"other_info"` +} diff --git a/internal/model/share.go b/internal/model/share.go index 6e89dbb..fed9c15 100644 --- a/internal/model/share.go +++ b/internal/model/share.go @@ -10,11 +10,11 @@ type Share struct { ExpiresIn int `json:"expiresIn" gorm:"column:expires_in"` ExpiresAt string `json:"expiresAt" gorm:"column:expires_at"` SiteLimit string `json:"siteLimit" gorm:"column:site_limit"` - Gpt4Limit int `json:"gpt4Limit" gorm:"column:gpt4_limit"` - Gpt4oLimit int `json:"gpt4oLimit" gorm:"column:gpt4o_limit"` - Gpt4oMiniLimit int `json:"gpt4oMiniLimit" gorm:"column:gpt4o_mini_limit"` - O1Limit int `json:"o1Limit" gorm:"column:o1_limit"` - O1MiniLimit int `json:"o1MiniLimit" gorm:"column:o1_mini_limit"` + Gpt4Limit int `json:"gpt4Limit" gorm:"column:gpt4_limit;default:-1"` + Gpt4oLimit int `json:"gpt4oLimit" gorm:"column:gpt4o_limit;default:-1"` + Gpt4oMiniLimit int `json:"gpt4oMiniLimit" gorm:"column:gpt4o_mini_limit;default:-1"` + O1Limit int `json:"o1Limit" gorm:"column:o1_limit;default:-1"` + O1MiniLimit int `json:"o1MiniLimit" gorm:"column:o1_mini_limit;default:-1"` Gpt35Limit int `json:"gpt35Limit" gorm:"column:gpt35_limit"` ShowUserinfo bool `json:"showUserinfo" gorm:"column:show_userinfo"` ShowConversations bool `json:"showConversations" gorm:"column:show_conversations"` diff --git a/internal/server/http.go b/internal/server/http.go index eccb11d..7d4011c 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -131,6 +131,7 @@ func NewHTTPServer( accountAuthRouter.POST("/search", accountHandler.SearchAccount) accountAuthRouter.POST("/delete", accountHandler.DeleteAccount) accountAuthRouter.POST("/update", accountHandler.UpdateAccount) + accountAuthRouter.POST("/oneapi/channels", accountHandler.GetOneApiChannelList) } } diff --git a/internal/service/account.go b/internal/service/account.go index bda44cf..522df08 100644 --- a/internal/service/account.go +++ b/internal/service/account.go @@ -11,6 +11,7 @@ import ( "github.com/go-resty/resty/v2" "github.com/spf13/viper" "go.uber.org/zap" + "strconv" ) type AccountService interface { @@ -22,6 +23,8 @@ type AccountService interface { DeleteAccount(ctx context.Context, id int64) error GetShareAccountList(ctx *gin.Context) ([]*model.Account, bool, bool, error) LoginShareAccount(ctx *gin.Context, req *v1.LoginShareAccountRequest) (string, error) + GetOneApiChannelList(ctx context.Context) ([]*model.OneApiChannel, error) + UpdateOneApiChannelToken(ctx context.Context, id int64, token string) error } func NewAccountService(service *Service, accountRepository repository.AccountRepository, viper *viper.Viper, coordinator *Coordinator) AccountService { @@ -40,6 +43,66 @@ type accountService struct { coordinator *Coordinator } +func (s *accountService) UpdateOneApiChannelToken(ctx context.Context, id int64, token string) error { + if s.viper.GetString("oneapi.token") == "" || s.viper.GetString("oneapi.domain") == "" { + s.logger.Warn("oneapi token is empty, disable oneapi channel") + return nil + } + oneToken := s.viper.GetString("oneapi.token") + oneUrl := fmt.Sprintf("%s/api/channel", s.viper.GetString("oneapi.domain")) + + client := resty.New() + + getUrl := fmt.Sprintf("%s/%d", oneUrl, id) + resp := struct { + Data model.OneApiChannel `json:"data"` + }{ + Data: model.OneApiChannel{}, + } + + res, err := client.R().SetHeader("Authorization", "Bearer "+oneToken).SetResult(&resp).Get(getUrl) + if err != nil { + return err + } + s.logger.Info("GetOneApiChannel", zap.Any("result", res)) + + param := resp.Data + param.Key = token + + res, err = client.R().SetHeader("Authorization", "Bearer "+oneToken).SetBody(param).Put(oneUrl) + + s.logger.Info("UpdateOneApiChannelToken", zap.Any("result", resp)) + if err != nil { + return err + } + return nil +} + +func (s *accountService) GetOneApiChannelList(ctx context.Context) ([]*model.OneApiChannel, error) { + // 检测是否有oneapi的token不为空 + if s.viper.GetString("oneapi.token") == "" || s.viper.GetString("oneapi.domain") == "" { + s.logger.Warn("oneapi token is empty, disable oneapi channel") + return []*model.OneApiChannel{}, nil + } + oneToken := s.viper.GetString("oneapi.token") + oneUrl := fmt.Sprintf("%s/api/channel/?p=0&page_size=1000&id_sort=true", s.viper.GetString("oneapi.domain")) + + res := struct { + Data []*model.OneApiChannel `json:"data"` + }{ + Data: make([]*model.OneApiChannel, 0), + } + client := resty.New() + resp, err := client.R().SetHeader("Authorization", oneToken).SetResult(&res).Get(oneUrl) + if err != nil { + return nil, err + } + // 取data字段 + result := res.Data + s.logger.Info("GetOneApiChannelList", zap.Any("result", resp)) + return result, nil +} + func (s *accountService) LoginShareAccount(ctx *gin.Context, req *v1.LoginShareAccountRequest) (string, error) { account, err := s.accountRepository.GetAccount(ctx, req.Id) if err != nil { @@ -152,6 +215,14 @@ func (s *accountService) RefreshAccount(ctx context.Context, id int64) error { return err } } + // 使用新的accessToken 刷新对接OneApi的渠道Token + if account.AccountType == "chatgpt" && account.OneApiChannelId != "" { + channelId, err := strconv.Atoi(account.OneApiChannelId) + err = s.UpdateOneApiChannelToken(ctx, int64(channelId), accessToken) + if err != nil { + return err + } + } return nil }