Compare commits

...

5 Commits

Author SHA1 Message Date
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
16 changed files with 159 additions and 29 deletions

View File

@@ -57,7 +57,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
+ [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI` + [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`
+ [x] [OpenAI-SB](https://openai-sb.com) + [x] [OpenAI-SB](https://openai-sb.com)
+ [x] [API2GPT](http://console.api2gpt.com/m/00002S) + [x] [API2GPT](http://console.api2gpt.com/m/00002S)
+ [x] [CloseAI](https://console.openai-asia.com/r/2412) + [x] [CloseAI](https://console.closeai-asia.com/r/2412)
+ [x] [AI.LS](https://ai.ls) + [x] [AI.LS](https://ai.ls)
+ [x] [OpenAI Max](https://openaimax.com) + [x] [OpenAI Max](https://openaimax.com)
+ [x] 自定义渠道:例如各种未收录的第三方代理服务 + [x] 自定义渠道:例如各种未收录的第三方代理服务
@@ -155,6 +155,12 @@ sudo service nginx restart
环境变量的具体使用方法详见[此处](#环境变量)。 环境变量的具体使用方法详见[此处](#环境变量)。
### 宝塔部署教程
详见[#175](https://github.com/songquanpeng/one-api/issues/175)。
如果部署后访问出现空白页面,详见[#97](https://github.com/songquanpeng/one-api/issues/97)。
### 部署第三方服务配合 One API 使用 ### 部署第三方服务配合 One API 使用
> 欢迎 PR 添加更多示例。 > 欢迎 PR 添加更多示例。
@@ -259,9 +265,7 @@ https://openai.justsong.cn
1. 账户额度足够为什么提示额度不足? 1. 账户额度足够为什么提示额度不足?
+ 请检查你的令牌额度是否足够,这个和账户额度是分开的。 + 请检查你的令牌额度是否足够,这个和账户额度是分开的。
+ 令牌额度仅供用户设置最大使用量,用户可自由设置。 + 令牌额度仅供用户设置最大使用量,用户可自由设置。
2. 宝塔部署后访问出现空白页面 2. 提示无可用渠道
+ 自动配置的问题,详见[#97](https://github.com/songquanpeng/one-api/issues/97)。
3. 提示无可用渠道?
+ 请检查的用户分组和渠道分组设置。 + 请检查的用户分组和渠道分组设置。
+ 以及渠道的模型设置。 + 以及渠道的模型设置。

View File

@@ -55,6 +55,8 @@ var TurnstileSiteKey = ""
var TurnstileSecretKey = "" var TurnstileSecretKey = ""
var QuotaForNewUser = 0 var QuotaForNewUser = 0
var QuotaForInviter = 0
var QuotaForInvitee = 0
var ChannelDisableThreshold = 5.0 var ChannelDisableThreshold = 5.0
var AutomaticDisableChannelEnabled = false var AutomaticDisableChannelEnabled = false
var QuotaRemindThreshold = 1000 var QuotaRemindThreshold = 1000
@@ -138,17 +140,17 @@ const (
) )
var ChannelBaseURLs = []string{ var ChannelBaseURLs = []string{
"", // 0 "", // 0
"https://api.openai.com", // 1 "https://api.openai.com", // 1
"https://oa.api2d.net", // 2 "https://oa.api2d.net", // 2
"", // 3 "", // 3
"https://api.openai-asia.com", // 4 "https://api.openai-proxy.org", // 4
"https://api.openai-sb.com", // 5 "https://api.openai-sb.com", // 5
"https://api.openaimax.com", // 6 "https://api.openaimax.com", // 6
"https://api.ohmygpt.com", // 7 "https://api.ohmygpt.com", // 7
"", // 8 "", // 8
"https://api.caipacity.com", // 9 "https://api.caipacity.com", // 9
"https://api.aiproxy.io", // 10 "https://api.aiproxy.io", // 10
"", // 11 "", // 11
"https://api.api2gpt.com", // 12 "https://api.api2gpt.com", // 12
} }

View File

@@ -157,6 +157,15 @@ func GenerateKey() string {
return string(key) 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 { func GetTimestamp() int64 {
return time.Now().Unix() return time.Now().Unix()
} }

View File

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

View File

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

View File

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

View File

@@ -56,6 +56,8 @@ func InitOptionMap() {
common.OptionMap["TurnstileSiteKey"] = "" common.OptionMap["TurnstileSiteKey"] = ""
common.OptionMap["TurnstileSecretKey"] = "" common.OptionMap["TurnstileSecretKey"] = ""
common.OptionMap["QuotaForNewUser"] = strconv.Itoa(common.QuotaForNewUser) 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["QuotaRemindThreshold"] = strconv.Itoa(common.QuotaRemindThreshold)
common.OptionMap["PreConsumedQuota"] = strconv.Itoa(common.PreConsumedQuota) common.OptionMap["PreConsumedQuota"] = strconv.Itoa(common.PreConsumedQuota)
common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString() common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString()
@@ -175,6 +177,10 @@ func updateOptionMap(key string, value string) (err error) {
common.TurnstileSecretKey = value common.TurnstileSecretKey = value
case "QuotaForNewUser": case "QuotaForNewUser":
common.QuotaForNewUser, _ = strconv.Atoi(value) common.QuotaForNewUser, _ = strconv.Atoi(value)
case "QuotaForInviter":
common.QuotaForInviter, _ = strconv.Atoi(value)
case "QuotaForInvitee":
common.QuotaForInvitee, _ = strconv.Atoi(value)
case "QuotaRemindThreshold": case "QuotaRemindThreshold":
common.QuotaRemindThreshold, _ = strconv.Atoi(value) common.QuotaRemindThreshold, _ = strconv.Atoi(value)
case "PreConsumedQuota": case "PreConsumedQuota":

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react'; import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react';
import { Link } from 'react-router-dom'; 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'; import Turnstile from 'react-turnstile';
const PersonalSetting = () => { 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 () => { const bindWeChat = async () => {
if (inputs.wechat_verification_code === '') return; if (inputs.wechat_verification_code === '') return;
const res = await API.get( const res = await API.get(
@@ -110,6 +122,7 @@ const PersonalSetting = () => {
更新个人信息 更新个人信息
</Button> </Button>
<Button onClick={generateAccessToken}>生成系统访问令牌</Button> <Button onClick={generateAccessToken}>生成系统访问令牌</Button>
<Button onClick={getAffLink}>复制邀请链接</Button>
<Divider /> <Divider />
<Header as='h3'>账号绑定</Header> <Header as='h3'>账号绑定</Header>
{ {

View File

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

View File

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

View File

@@ -27,6 +27,8 @@ const SystemSetting = () => {
TurnstileSecretKey: '', TurnstileSecretKey: '',
RegisterEnabled: '', RegisterEnabled: '',
QuotaForNewUser: 0, QuotaForNewUser: 0,
QuotaForInviter: 0,
QuotaForInvitee: 0,
QuotaRemindThreshold: 0, QuotaRemindThreshold: 0,
PreConsumedQuota: 0, PreConsumedQuota: 0,
ModelRatio: '', ModelRatio: '',
@@ -34,7 +36,7 @@ const SystemSetting = () => {
TopUpLink: '', TopUpLink: '',
AutomaticDisableChannelEnabled: '', AutomaticDisableChannelEnabled: '',
ChannelDisableThreshold: 0, ChannelDisableThreshold: 0,
LogConsumeEnabled: '', LogConsumeEnabled: ''
}); });
const [originInputs, setOriginInputs] = useState({}); const [originInputs, setOriginInputs] = useState({});
let [loading, setLoading] = useState(false); let [loading, setLoading] = useState(false);
@@ -101,6 +103,8 @@ const SystemSetting = () => {
name === 'TurnstileSiteKey' || name === 'TurnstileSiteKey' ||
name === 'TurnstileSecretKey' || name === 'TurnstileSecretKey' ||
name === 'QuotaForNewUser' || name === 'QuotaForNewUser' ||
name === 'QuotaForInviter' ||
name === 'QuotaForInvitee' ||
name === 'QuotaRemindThreshold' || name === 'QuotaRemindThreshold' ||
name === 'PreConsumedQuota' || name === 'PreConsumedQuota' ||
name === 'ModelRatio' || name === 'ModelRatio' ||
@@ -122,6 +126,12 @@ const SystemSetting = () => {
if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) { if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
await updateOption('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) { if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) {
await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold); await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold);
} }
@@ -329,6 +339,28 @@ const SystemSetting = () => {
placeholder='请求结束后多退少补' placeholder='请求结束后多退少补'
/> />
</Form.Group> </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.Group>
<Form.Group widths='equal'> <Form.Group widths='equal'>
<Form.TextArea <Form.TextArea
label='模型倍率' label='模型倍率'

View File

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

View File

@@ -156,7 +156,7 @@ const UsersTable = () => {
/> />
</Form> </Form>
<Table basic> <Table basic compact size='small'>
<Table.Header> <Table.Header>
<Table.Row> <Table.Row>
<Table.HeaderCell <Table.HeaderCell
@@ -240,7 +240,9 @@ const UsersTable = () => {
/> />
</Table.Cell> </Table.Cell>
<Table.Cell>{renderGroup(user.group)}</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> <Table.Cell>
<Popup content='剩余额度' trigger={<Label>{renderNumber(user.quota)}</Label>} /> <Popup content='剩余额度' trigger={<Label>{renderNumber(user.quota)}</Label>} />
<Popup content='已用额度' trigger={<Label>{renderNumber(user.used_quota)}</Label>} /> <Popup content='已用额度' trigger={<Label>{renderNumber(user.used_quota)}</Label>} />