Compare commits

...

19 Commits

Author SHA1 Message Date
JustSong
dc94765d32 fix: do not print response to console 2023-06-18 00:22:22 +08:00
JustSong
1cb1f727c0 fix: fix the shared field is modified 2023-06-18 00:20:06 +08:00
JustSong
d97640374c feat: able to add chat page link now (close #70) 2023-06-17 23:51:56 +08:00
JustSong
ba89abedf0 docs: update README 2023-06-17 23:18:27 +08:00
JustSong
a680b1b8b7 docs: update issue template 2023-06-17 22:57:46 +08:00
JustSong
b3b7d0a0ea docs: update README 2023-06-17 22:56:12 +08:00
JustSong
8e805e23bc chore: update prompt 2023-06-17 19:33:25 +08:00
JustSong
bcbfacc04a fix: reduce the table size (close #174) 2023-06-17 19:23:25 +08:00
JustSong
5531e21526 docs: update README (close #175) 2023-06-17 19:08:13 +08:00
JustSong
c5837c3bb7 feat: support aff now (close #75) 2023-06-17 18:12:58 +08:00
JustSong
eb70b84665 chore: update api endpoint for CloseAI 2023-06-17 15:54:14 +08:00
JustSong
a909972313 fix: limit the length of email 2023-06-17 15:44:04 +08:00
JustSong
6855d0dc39 chore: add x-requested-with header in CORS setting 2023-06-17 15:30:14 +08:00
Kidultx
a43b1e2add feat: support API2GPT platform (#173)
* support API2GPT platform

* chore: update balance renderer

---------

Co-authored-by: JustSong <songquanpeng@foxmail.com>
2023-06-17 15:20:51 +08:00
Miniers
46c43396d8 feat: add token name to log (#172)
* add token name to log

* chore: update expression

---------

Co-authored-by: JustSong <songquanpeng@foxmail.com>
2023-06-17 14:56:03 +08:00
JustSong
6dcffca065 docs: update README 2023-06-17 12:55:48 +08:00
JustSong
d754620ef7 docs: update README 2023-06-17 12:07:58 +08:00
JustSong
21111126a2 docs: update README 2023-06-17 11:30:31 +08:00
JustSong
d91e7dcfdc docs: update README 2023-06-17 11:24:32 +08:00
28 changed files with 312 additions and 62 deletions

View File

@@ -10,6 +10,7 @@ assignees: ''
**例行检查**
+ [ ] 我已确认目前没有类似 issue
+ [ ] 我已确认我已升级到最新版本
+ [ ] 我已完整查看过项目 README尤其是常见问题部分
+ [ ] 我理解并愿意跟进此 issue协助测试和提供反馈
+ [ ] 我理解并认可上述内容,并理解项目维护者精力有限,不遵循规则的 issue 可能会被无视或直接关闭

View File

@@ -55,10 +55,11 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
+ [x] [API2D](https://api2d.com/r/197971)
+ [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf)
+ [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`
+ [x] [OpenAI-SB](https://openai-sb.com)
+ [x] [API2GPT](http://console.api2gpt.com/m/00002S)
+ [x] [CloseAI](https://console.closeai-asia.com/r/2412)
+ [x] [AI.LS](https://ai.ls)
+ [x] [OpenAI Max](https://openaimax.com)
+ [x] [OpenAI-SB](https://openai-sb.com)
+ [x] [CloseAI](https://console.openai-asia.com/r/2412)
+ [x] 自定义渠道:例如各种未收录的第三方代理服务
2. 支持通过**负载均衡**的方式访问多个渠道。
3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。
@@ -74,15 +75,18 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
1. 支持自定义系统名称logo 以及页脚。
2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
13. 支持通过系统访问令牌访问管理 API。
14. 支持用户管理,支持**多种用户登录注册方式**
14. 支持 Cloudflare Turnstile 用户校验。
15. 支持用户管理,支持**多种用户登录注册方式**
+ 邮箱登录注册以及通过邮箱进行密码重置。
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
15. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
16. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
## 部署
### 基于 Docker 进行部署
执行`docker run -d --restart always -p 3000:3000 -v /home/ubuntu/data/one-api:/data justsong/one-api`
部署命令`docker run --name one-api -d --restart always -p 3000:3000 -v /home/ubuntu/data/one-api:/data justsong/one-api`
更新命令:`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR`
`-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。
@@ -151,6 +155,32 @@ sudo service nginx restart
环境变量的具体使用方法详见[此处](#环境变量)。
### 宝塔部署教程
详见[#175](https://github.com/songquanpeng/one-api/issues/175)。
如果部署后访问出现空白页面,详见[#97](https://github.com/songquanpeng/one-api/issues/97)。
### 部署第三方服务配合 One API 使用
> 欢迎 PR 添加更多示例。
#### ChatGPT Next Web
项目主页https://github.com/Yidadaa/ChatGPT-Next-Web
```bash
docker run --name chat-next-web -d -p 3001:3000 -e BASE_URL=https://openai.justsong.cn yidadaa/chatgpt-next-web
```
注意修改端口号和 `BASE_URL`。
#### ChatGPT Web
项目主页https://github.com/Chanzhaoyu/chatgpt-web
```bash
docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://openai.justsong.cn -e OPENAI_API_KEY=sk-xxx chenzhaoyu94/chatgpt-web
```
注意修改端口号、`OPENAI_API_BASE_URL` 和 `OPENAI_API_KEY`。
### 部署到第三方平台
<details>
@@ -207,7 +237,7 @@ graph LR
+ 例子:`REDIS_CONN_STRING=redis://default:redispw@localhost:49153`
2. `SESSION_SECRET`:设置之后将使用固定的会话密钥,这样系统重新启动后已登录用户的 cookie 将依旧有效。
+ 例子:`SESSION_SECRET=random_string`
3. `SQL_DSN`:设置之后将使用指定数据库而非 SQLite。
3. `SQL_DSN`:设置之后将使用指定数据库而非 SQLite,请使用 MySQL 8.0 版本
+ 例子:`SQL_DSN=root:123456@tcp(localhost:3306)/one-api`
4. `FRONTEND_BASE_URL`:设置之后将使用指定的前端地址,而非后端地址。
+ 例子:`FRONTEND_BASE_URL=https://openai.justsong.cn`
@@ -232,14 +262,18 @@ https://openai.justsong.cn
![token](https://user-images.githubusercontent.com/39998050/233837971-dab488b7-6d96-43af-b640-a168e8d1c9bf.png)
## 常见问题
1. 账户额度足够为什么提示额度不足
1. 额度是什么?怎么计算的
+ 额度 = token * 倍率
+ 倍率包括分组的倍率,以及补全的倍率。
2. 账户额度足够为什么提示额度不足?
+ 请检查你的令牌额度是否足够,这个和账户额度是分开的。
+ 令牌额度仅供用户设置最大使用量,用户可自由设置。
2. 宝塔部署后访问出现空白页面?
+ 自动配置的问题,详见[#97](https://github.com/songquanpeng/one-api/issues/97)。
3. 提示无可用渠道?
+ 请检查的用户分组和渠道分组设置。
+ 以及渠道的模型设置。
4. 渠道测试报错:`invalid character '<' looking for beginning of value`
+ 这是因为返回值不是合法的 JSON而是一个 HTML 页面。
+ 大概率是你的部署站的 IP 或代理的节点被 CloudFlare 封禁了。
## 注意
本项目为开源项目,请在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及法律法规的情况下使用,不得用于非法用途。

View File

@@ -1,9 +1,10 @@
package common
import (
"github.com/google/uuid"
"sync"
"time"
"github.com/google/uuid"
)
var StartTime = time.Now().Unix() // unit: second
@@ -13,6 +14,7 @@ var ServerAddress = "http://localhost:3000"
var Footer = ""
var Logo = ""
var TopUpLink = ""
var ChatLink = ""
var UsingSQLite = false
@@ -54,6 +56,8 @@ var TurnstileSiteKey = ""
var TurnstileSecretKey = ""
var QuotaForNewUser = 0
var QuotaForInviter = 0
var QuotaForInvitee = 0
var ChannelDisableThreshold = 5.0
var AutomaticDisableChannelEnabled = false
var QuotaRemindThreshold = 1000
@@ -133,19 +137,21 @@ const (
ChannelTypeAILS = 9
ChannelTypeAIProxy = 10
ChannelTypePaLM = 11
ChannelTypeAPI2GPT = 12
)
var ChannelBaseURLs = []string{
"", // 0
"https://api.openai.com", // 1
"https://oa.api2d.net", // 2
"", // 3
"https://api.openai-asia.com", // 4
"https://api.openai-sb.com", // 5
"https://api.openaimax.com", // 6
"https://api.ohmygpt.com", // 7
"", // 8
"https://api.caipacity.com", // 9
"https://api.aiproxy.io", // 10
"", // 11
"", // 0
"https://api.openai.com", // 1
"https://oa.api2d.net", // 2
"", // 3
"https://api.openai-proxy.org", // 4
"https://api.openai-sb.com", // 5
"https://api.openaimax.com", // 6
"https://api.ohmygpt.com", // 7
"", // 8
"https://api.caipacity.com", // 9
"https://api.aiproxy.io", // 10
"", // 11
"https://api.api2gpt.com", // 12
}

View File

@@ -157,6 +157,15 @@ func GenerateKey() string {
return string(key)
}
func GetRandomString(length int) string {
rand.Seed(time.Now().UnixNano())
key := make([]byte, length)
for i := 0; i < length; i++ {
key[i] = keyChars[rand.Intn(len(keyChars))]
}
return string(key)
}
func GetTimestamp() int64 {
return time.Now().Unix()
}

View File

@@ -54,6 +54,13 @@ type AIProxyUserOverviewResponse struct {
} `json:"data"`
}
type API2GPTUsageResponse struct {
Object string `json:"object"`
TotalGranted float64 `json:"total_granted"`
TotalUsed float64 `json:"total_used"`
TotalRemaining float64 `json:"total_remaining"`
}
// GetAuthHeader get auth header
func GetAuthHeader(token string) http.Header {
h := http.Header{}
@@ -127,6 +134,22 @@ func updateChannelAIProxyBalance(channel *model.Channel) (float64, error) {
return response.Data.TotalPoints, nil
}
func updateChannelAPI2GPTBalance(channel *model.Channel) (float64, error) {
url := "https://api.api2gpt.com/dashboard/billing/credit_grants"
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
if err != nil {
return 0, err
}
response := API2GPTUsageResponse{}
err = json.Unmarshal(body, &response)
if err != nil {
return 0, err
}
channel.UpdateBalance(response.TotalRemaining)
return response.TotalRemaining, nil
}
func updateChannelBalance(channel *model.Channel) (float64, error) {
baseURL := common.ChannelBaseURLs[channel.Type]
switch channel.Type {
@@ -142,6 +165,8 @@ func updateChannelBalance(channel *model.Channel) (float64, error) {
return updateChannelOpenAISBBalance(channel)
case common.ChannelTypeAIProxy:
return updateChannelAIProxyBalance(channel)
case common.ChannelTypeAPI2GPT:
return updateChannelAPI2GPTBalance(channel)
default:
return 0, errors.New("尚未实现")
}

View File

@@ -14,12 +14,12 @@ import (
"time"
)
func testChannel(channel *model.Channel, request *ChatRequest) error {
if request.Model == "" {
func testChannel(channel *model.Channel, request ChatRequest) error {
switch channel.Type {
case common.ChannelTypeAzure:
request.Model = "gpt-35-turbo"
default:
request.Model = "gpt-3.5-turbo"
if channel.Type == common.ChannelTypeAzure {
request.Model = "gpt-35-turbo"
}
}
requestURL := common.ChannelBaseURLs[channel.Type]
if channel.Type == common.ChannelTypeAzure {
@@ -97,7 +97,7 @@ func TestChannel(c *gin.Context) {
}
testRequest := buildTestRequest(c)
tik := time.Now()
err = testChannel(channel, testRequest)
err = testChannel(channel, *testRequest)
tok := time.Now()
milliseconds := tok.Sub(tik).Milliseconds()
go channel.UpdateResponseTime(milliseconds)
@@ -165,7 +165,7 @@ func testAllChannels(c *gin.Context) error {
continue
}
tik := time.Now()
err := testChannel(channel, testRequest)
err := testChannel(channel, *testRequest)
tok := time.Now()
milliseconds := tok.Sub(tik).Milliseconds()
if err != nil || milliseconds > disableThreshold {

View File

@@ -125,7 +125,7 @@ func GitHubOAuth(c *gin.Context) {
user.Role = common.RoleCommonUser
user.Status = common.UserStatusEnabled
if err := user.Insert(); err != nil {
if err := user.Insert(0); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),

View File

@@ -28,6 +28,7 @@ func GetStatus(c *gin.Context) {
"turnstile_check": common.TurnstileCheckEnabled,
"turnstile_site_key": common.TurnstileSiteKey,
"top_up_link": common.TopUpLink,
"chat_link": common.ChatLink,
},
})
return

View File

@@ -259,8 +259,9 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
if err != nil {
common.SysError("Error consuming token remain quota: " + err.Error())
}
tokenName := c.GetString("token_name")
userId := c.GetInt("id")
model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("使用模型 %s 消耗 %d 点额度(模型倍率 %.2f,分组倍率 %.2f", textRequest.Model, quota, modelRatio, groupRatio))
model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("通过令牌「%s」使用模型 %s 消耗 %d 点额度(模型倍率 %.2f,分组倍率 %.2f", tokenName, textRequest.Model, quota, modelRatio, groupRatio))
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
channelId := c.GetInt("channel_id")
model.UpdateChannelUsedQuota(channelId, quota)

View File

@@ -150,15 +150,18 @@ func Register(c *gin.Context) {
return
}
}
affCode := user.AffCode // this code is the inviter's code, not the user's own code
inviterId, _ := model.GetUserIdByAffCode(affCode)
cleanUser := model.User{
Username: user.Username,
Password: user.Password,
DisplayName: user.Username,
InviterId: inviterId,
}
if common.EmailVerificationEnabled {
cleanUser.Email = user.Email
}
if err := cleanUser.Insert(); err != nil {
if err := cleanUser.Insert(inviterId); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
@@ -280,6 +283,34 @@ func GenerateAccessToken(c *gin.Context) {
return
}
func GetAffCode(c *gin.Context) {
id := c.GetInt("id")
user, err := model.GetUserById(id, true)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
if user.AffCode == "" {
user.AffCode = common.GetRandomString(4)
if err := user.Update(false); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": user.AffCode,
})
return
}
func GetSelf(c *gin.Context) {
id := c.GetInt("id")
user, err := model.GetUserById(id, false)
@@ -495,7 +526,7 @@ func CreateUser(c *gin.Context) {
Password: user.Password,
DisplayName: user.DisplayName,
}
if err := cleanUser.Insert(); err != nil {
if err := cleanUser.Insert(0); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),

View File

@@ -85,7 +85,7 @@ func WeChatAuth(c *gin.Context) {
user.Role = common.RoleCommonUser
user.Status = common.UserStatusEnabled
if err := user.Insert(); err != nil {
if err := user.Insert(0); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),

View File

@@ -112,6 +112,7 @@ func TokenAuth() func(c *gin.Context) {
}
c.Set("id", token.UserId)
c.Set("token_id", token.Id)
c.Set("token_name", token.Name)
requestURL := c.Request.URL.String()
consumeQuota := true
if strings.HasPrefix(requestURL, "/v1/models") {

View File

@@ -10,6 +10,6 @@ func CORS() gin.HandlerFunc {
config.AllowAllOrigins = true
config.AllowCredentials = true
config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}
config.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization", "Accept", "Connection"}
config.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization", "Accept", "Connection", "x-requested-with"}
return cors.New(config)
}

View File

@@ -56,11 +56,14 @@ func InitOptionMap() {
common.OptionMap["TurnstileSiteKey"] = ""
common.OptionMap["TurnstileSecretKey"] = ""
common.OptionMap["QuotaForNewUser"] = strconv.Itoa(common.QuotaForNewUser)
common.OptionMap["QuotaForInviter"] = strconv.Itoa(common.QuotaForInviter)
common.OptionMap["QuotaForInvitee"] = strconv.Itoa(common.QuotaForInvitee)
common.OptionMap["QuotaRemindThreshold"] = strconv.Itoa(common.QuotaRemindThreshold)
common.OptionMap["PreConsumedQuota"] = strconv.Itoa(common.PreConsumedQuota)
common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString()
common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString()
common.OptionMap["TopUpLink"] = common.TopUpLink
common.OptionMap["ChatLink"] = common.ChatLink
common.OptionMapRWMutex.Unlock()
loadOptionsFromDatabase()
}
@@ -175,6 +178,10 @@ func updateOptionMap(key string, value string) (err error) {
common.TurnstileSecretKey = value
case "QuotaForNewUser":
common.QuotaForNewUser, _ = strconv.Atoi(value)
case "QuotaForInviter":
common.QuotaForInviter, _ = strconv.Atoi(value)
case "QuotaForInvitee":
common.QuotaForInvitee, _ = strconv.Atoi(value)
case "QuotaRemindThreshold":
common.QuotaRemindThreshold, _ = strconv.Atoi(value)
case "PreConsumedQuota":
@@ -185,6 +192,8 @@ func updateOptionMap(key string, value string) (err error) {
err = common.UpdateGroupRatioByJSONString(value)
case "TopUpLink":
common.TopUpLink = value
case "ChatLink":
common.ChatLink = value
case "ChannelDisableThreshold":
common.ChannelDisableThreshold, _ = strconv.ParseFloat(value, 64)
}

View File

@@ -26,6 +26,8 @@ type User struct {
UsedQuota int `json:"used_quota" gorm:"type:int;default:0;column:used_quota"` // used quota
RequestCount int `json:"request_count" gorm:"type:int;default:0;"` // request number
Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"`
InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"`
}
func GetMaxUserId() int {
@@ -58,6 +60,15 @@ func GetUserById(id int, selectAll bool) (*User, error) {
return &user, err
}
func GetUserIdByAffCode(affCode string) (int, error) {
if affCode == "" {
return 0, errors.New("affCode 为空!")
}
var user User
err := DB.Select("id").First(&user, "aff_code = ?", affCode).Error
return user.Id, err
}
func DeleteUserById(id int) (err error) {
if id == 0 {
return errors.New("id 为空!")
@@ -66,7 +77,7 @@ func DeleteUserById(id int) (err error) {
return user.Delete()
}
func (user *User) Insert() error {
func (user *User) Insert(inviterId int) error {
var err error
if user.Password != "" {
user.Password, err = common.Password2Hash(user.Password)
@@ -76,6 +87,7 @@ func (user *User) Insert() error {
}
user.Quota = common.QuotaForNewUser
user.AccessToken = common.GetUUID()
user.AffCode = common.GetRandomString(4)
result := DB.Create(user)
if result.Error != nil {
return result.Error
@@ -83,6 +95,16 @@ func (user *User) Insert() error {
if common.QuotaForNewUser > 0 {
RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %d 点额度", common.QuotaForNewUser))
}
if inviterId != 0 {
if common.QuotaForInvitee > 0 {
_ = IncreaseUserQuota(user.Id, common.QuotaForInvitee)
RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %d 点额度", common.QuotaForInvitee))
}
if common.QuotaForInviter > 0 {
_ = IncreaseUserQuota(inviterId, common.QuotaForInviter)
RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %d 点额度", common.QuotaForInviter))
}
}
return nil
}

View File

@@ -37,6 +37,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute.PUT("/self", controller.UpdateSelf)
selfRoute.DELETE("/self", controller.DeleteSelf)
selfRoute.GET("/token", controller.GenerateAccessToken)
selfRoute.GET("/aff", controller.GetAffCode)
selfRoute.POST("/topup", controller.TopUp)
}

View File

@@ -23,6 +23,7 @@ import Redemption from './pages/Redemption';
import EditRedemption from './pages/Redemption/EditRedemption';
import TopUp from './pages/TopUp';
import Log from './pages/Log';
import Chat from './pages/Chat';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
@@ -47,6 +48,11 @@ function App() {
localStorage.setItem('system_name', data.system_name);
localStorage.setItem('logo', data.logo);
localStorage.setItem('footer_html', data.footer_html);
if (data.chat_link) {
localStorage.setItem('chat_link', data.chat_link);
} else {
localStorage.removeItem('chat_link');
}
if (
data.version !== process.env.REACT_APP_VERSION &&
data.version !== 'v0.0.0' &&
@@ -267,6 +273,14 @@ function App() {
</Suspense>
}
/>
<Route
path='/chat'
element={
<Suspense fallback={<Loading></Loading>}>
<Chat />
</Suspense>
}
/>
<Route path='*' element={NotFound} />
</Routes>
);

View File

@@ -36,6 +36,8 @@ function renderBalance(type, balance) {
return <span>¥{(balance / 10000).toFixed(2)}</span>;
case 10: // AI Proxy
return <span>{renderNumber(balance)}</span>;
case 12: // API2GPT
return <span>¥{balance.toFixed(2)}</span>;
default:
return <span>不支持</span>;
}
@@ -260,7 +262,7 @@ const ChannelsTable = () => {
/>
</Form>
<Table basic>
<Table basic compact size='small'>
<Table.Header>
<Table.Row>
<Table.HeaderCell

View File

@@ -7,57 +7,65 @@ import { API, getLogo, getSystemName, isAdmin, isMobile, showSuccess } from '../
import '../index.css';
// Header Buttons
const headerButtons = [
let headerButtons = [
{
name: '首页',
to: '/',
icon: 'home',
icon: 'home'
},
{
name: '渠道',
to: '/channel',
icon: 'sitemap',
admin: true,
admin: true
},
{
name: '令牌',
to: '/token',
icon: 'key',
icon: 'key'
},
{
name: '兑换',
to: '/redemption',
icon: 'dollar sign',
admin: true,
admin: true
},
{
name: '充值',
to: '/topup',
icon: 'cart',
icon: 'cart'
},
{
name: '用户',
to: '/user',
icon: 'user',
admin: true,
admin: true
},
{
name: '日志',
to: '/log',
icon: 'book',
icon: 'book'
},
{
name: '设置',
to: '/setting',
icon: 'setting',
icon: 'setting'
},
{
name: '关于',
to: '/about',
icon: 'info circle',
},
icon: 'info circle'
}
];
if (localStorage.getItem('chat_link')) {
headerButtons.splice(1, 0, {
name: '聊天',
to: '/chat',
icon: 'comments'
});
}
const Header = () => {
const [userState, userDispatch] = useContext(UserContext);
let navigate = useNavigate();
@@ -112,11 +120,11 @@ const Header = () => {
style={
showSidebar
? {
borderBottom: 'none',
marginBottom: '0',
borderTop: 'none',
height: '51px',
}
borderBottom: 'none',
marginBottom: '0',
borderTop: 'none',
height: '51px'
}
: { borderTop: 'none', height: '52px' }
}
>

View File

@@ -165,7 +165,7 @@ const OtherSetting = () => {
/>
</Form.Group>
<Form.Button onClick={submitAbout}>保存关于</Form.Button>
<Message>移除 One API 的版权标识必须首先获得授权后续版本将通过授权码强制执行</Message>
<Message>移除 One API 的版权标识必须首先获得授权项目维护需要花费大量精力如果本项目对你有意义请主动支持本项目</Message>
<Form.Group widths='equal'>
<Form.Input
label='页脚'

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { API, copy, showError, showInfo, showSuccess } from '../helpers';
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
import Turnstile from 'react-turnstile';
const PersonalSetting = () => {
@@ -45,6 +45,18 @@ const PersonalSetting = () => {
}
};
const getAffLink = async () => {
const res = await API.get('/api/user/aff');
const { success, message, data } = res.data;
if (success) {
let link = `${window.location.origin}/register?aff=${data}`;
await copy(link);
showNotice(`邀请链接已复制到剪切板:${link}`);
} else {
showError(message);
}
};
const bindWeChat = async () => {
if (inputs.wechat_verification_code === '') return;
const res = await API.get(
@@ -110,6 +122,7 @@ const PersonalSetting = () => {
更新个人信息
</Button>
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
<Button onClick={getAffLink}>复制邀请链接</Button>
<Divider />
<Header as='h3'>账号绑定</Header>
{

View File

@@ -152,7 +152,7 @@ const RedemptionsTable = () => {
/>
</Form>
<Table basic>
<Table basic compact size='small'>
<Table.Header>
<Table.Row>
<Table.HeaderCell

View File

@@ -27,6 +27,10 @@ const RegisterForm = () => {
const [turnstileToken, setTurnstileToken] = useState('');
const [loading, setLoading] = useState(false);
const logo = getLogo();
let affCode = new URLSearchParams(window.location.search).get('aff');
if (affCode) {
localStorage.setItem('aff', affCode);
}
useEffect(() => {
let status = localStorage.getItem('status');
@@ -63,6 +67,10 @@ const RegisterForm = () => {
return;
}
setLoading(true);
if (!affCode) {
affCode = localStorage.getItem('aff');
}
inputs.aff_code = affCode;
const res = await API.post(
`/api/user/register?turnstile=${turnstileToken}`,
inputs

View File

@@ -27,14 +27,17 @@ const SystemSetting = () => {
TurnstileSecretKey: '',
RegisterEnabled: '',
QuotaForNewUser: 0,
QuotaForInviter: 0,
QuotaForInvitee: 0,
QuotaRemindThreshold: 0,
PreConsumedQuota: 0,
ModelRatio: '',
GroupRatio: '',
TopUpLink: '',
ChatLink: '',
AutomaticDisableChannelEnabled: '',
ChannelDisableThreshold: 0,
LogConsumeEnabled: '',
LogConsumeEnabled: ''
});
const [originInputs, setOriginInputs] = useState({});
let [loading, setLoading] = useState(false);
@@ -101,11 +104,14 @@ const SystemSetting = () => {
name === 'TurnstileSiteKey' ||
name === 'TurnstileSecretKey' ||
name === 'QuotaForNewUser' ||
name === 'QuotaForInviter' ||
name === 'QuotaForInvitee' ||
name === 'QuotaRemindThreshold' ||
name === 'PreConsumedQuota' ||
name === 'ModelRatio' ||
name === 'GroupRatio' ||
name === 'TopUpLink'
name === 'TopUpLink' ||
name === 'ChatLink'
) {
setInputs((inputs) => ({ ...inputs, [name]: value }));
} else {
@@ -122,6 +128,12 @@ const SystemSetting = () => {
if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
}
if (originInputs['QuotaForInvitee'] !== inputs.QuotaForInvitee) {
await updateOption('QuotaForInvitee', inputs.QuotaForInvitee);
}
if (originInputs['QuotaForInviter'] !== inputs.QuotaForInviter) {
await updateOption('QuotaForInviter', inputs.QuotaForInviter);
}
if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) {
await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold);
}
@@ -145,6 +157,9 @@ const SystemSetting = () => {
if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
await updateOption('TopUpLink', inputs.TopUpLink);
}
if (originInputs['ChatLink'] !== inputs.ChatLink) {
await updateOption('ChatLink', inputs.ChatLink);
}
};
const submitSMTP = async () => {
@@ -329,6 +344,37 @@ const SystemSetting = () => {
placeholder='请求结束后多退少补'
/>
</Form.Group>
<Form.Group widths={4}>
<Form.Input
label='邀请新用户奖励配额'
name='QuotaForInviter'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.QuotaForInviter}
type='number'
min='0'
placeholder='例如100'
/>
<Form.Input
label='新用户使用邀请码奖励配额'
name='QuotaForInvitee'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.QuotaForInvitee}
type='number'
min='0'
placeholder='例如100'
/>
<Form.Input
label='聊天页面链接'
name='ChatLink'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.ChatLink}
type='link'
placeholder='例如 ChatGPT Next Web 的部署地址'
/>
</Form.Group>
<Form.Group widths='equal'>
<Form.TextArea
label='模型倍率'

View File

@@ -161,7 +161,7 @@ const TokensTable = () => {
/>
</Form>
<Table basic>
<Table basic compact size='small'>
<Table.Header>
<Table.Row>
<Table.HeaderCell

View File

@@ -156,7 +156,7 @@ const UsersTable = () => {
/>
</Form>
<Table basic>
<Table basic compact size='small'>
<Table.Header>
<Table.Row>
<Table.HeaderCell
@@ -240,7 +240,9 @@ const UsersTable = () => {
/>
</Table.Cell>
<Table.Cell>{renderGroup(user.group)}</Table.Cell>
<Table.Cell>{user.email ? renderText(user.email, 30) : '无'}</Table.Cell>
<Table.Cell>
{user.email ? <Popup hoverable content={user.email} trigger={<span>{renderText(user.email, 24)}</span>} /> : '无'}
</Table.Cell>
<Table.Cell>
<Popup content='剩余额度' trigger={<Label>{renderNumber(user.quota)}</Label>} />
<Popup content='已用额度' trigger={<Label>{renderNumber(user.used_quota)}</Label>} />

View File

@@ -8,5 +8,6 @@ export const CHANNEL_OPTIONS = [
{ key: 6, text: 'OpenAI Max', value: 6, color: 'violet' },
{ key: 7, text: 'OhMyGPT', value: 7, color: 'purple' },
{ key: 9, text: 'AI.LS', value: 9, color: 'yellow' },
{ key: 10, text: 'AI Proxy', value: 10, color: 'purple' }
{ key: 10, text: 'AI Proxy', value: 10, color: 'purple' },
{ key: 12, text: 'API2GPT', value: 12, color: 'blue' }
];

View File

@@ -0,0 +1,15 @@
import React from 'react';
const Chat = () => {
const chatLink = localStorage.getItem('chat_link');
return (
<iframe
src={chatLink}
style={{ width: '100%', height: '85vh', border: 'none' }}
/>
);
};
export default Chat;