mirror of
https://github.com/linux-do/new-api.git
synced 2025-09-17 07:56:38 +08:00
feat: 令牌分组
This commit is contained in:
parent
5f3798053f
commit
052bc2075b
23
common/user_groups.go
Normal file
23
common/user_groups.go
Normal file
@ -0,0 +1,23 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
var UserUsableGroups = map[string]string{
|
||||
"default": "默认分组",
|
||||
"vip": "vip分组",
|
||||
}
|
||||
|
||||
func UserUsableGroups2JSONString() string {
|
||||
jsonBytes, err := json.Marshal(UserUsableGroups)
|
||||
if err != nil {
|
||||
SysError("error marshalling user groups: " + err.Error())
|
||||
}
|
||||
return string(jsonBytes)
|
||||
}
|
||||
|
||||
func UpdateUserUsableGroupsByJSONString(jsonStr string) error {
|
||||
UserUsableGroups = make(map[string]string)
|
||||
return json.Unmarshal([]byte(jsonStr), &UserUsableGroups)
|
||||
}
|
@ -17,3 +17,18 @@ func GetGroups(c *gin.Context) {
|
||||
"data": groupNames,
|
||||
})
|
||||
}
|
||||
|
||||
func GetUserGroups(c *gin.Context) {
|
||||
usableGroups := make(map[string]string)
|
||||
for groupName, _ := range common.GroupRatio {
|
||||
// UserUsableGroups contains the groups that the user can use
|
||||
if _, ok := common.UserUsableGroups[groupName]; ok {
|
||||
usableGroups[groupName] = common.UserUsableGroups[groupName]
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": usableGroups,
|
||||
})
|
||||
}
|
||||
|
@ -135,6 +135,7 @@ func AddToken(c *gin.Context) {
|
||||
ModelLimitsEnabled: token.ModelLimitsEnabled,
|
||||
ModelLimits: token.ModelLimits,
|
||||
AllowIps: token.AllowIps,
|
||||
Group: token.Group,
|
||||
}
|
||||
err = cleanToken.Insert()
|
||||
if err != nil {
|
||||
@ -223,6 +224,7 @@ func UpdateToken(c *gin.Context) {
|
||||
cleanToken.ModelLimitsEnabled = token.ModelLimitsEnabled
|
||||
cleanToken.ModelLimits = token.ModelLimits
|
||||
cleanToken.AllowIps = token.AllowIps
|
||||
cleanToken.Group = token.Group
|
||||
}
|
||||
err = cleanToken.Update()
|
||||
if err != nil {
|
||||
|
@ -176,6 +176,7 @@ func TokenAuth() func(c *gin.Context) {
|
||||
c.Set("token_model_limit_enabled", false)
|
||||
}
|
||||
c.Set("allow_ips", token.GetIpLimitsMap())
|
||||
c.Set("token_group", token.Group)
|
||||
if len(parts) > 1 {
|
||||
if model.IsAdmin(token.UserId) {
|
||||
c.Set("specific_channel_id", parts[1])
|
||||
|
@ -39,6 +39,15 @@ func Distribute() func(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
userGroup, _ := model.CacheGetUserGroup(userId)
|
||||
tokenGroup := c.GetString("token_group")
|
||||
if tokenGroup != "" {
|
||||
// check group in common.GroupRatio
|
||||
if _, ok := common.GroupRatio[tokenGroup]; !ok {
|
||||
abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("分组 %s 已被禁用", tokenGroup))
|
||||
return
|
||||
}
|
||||
userGroup = tokenGroup
|
||||
}
|
||||
c.Set("group", userGroup)
|
||||
if ok {
|
||||
id, err := strconv.Atoi(channelId.(string))
|
||||
|
@ -86,6 +86,7 @@ func InitOptionMap() {
|
||||
common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString()
|
||||
common.OptionMap["ModelPrice"] = common.ModelPrice2JSONString()
|
||||
common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString()
|
||||
common.OptionMap["UserUsableGroups"] = common.UserUsableGroups2JSONString()
|
||||
common.OptionMap["CompletionRatio"] = common.CompletionRatio2JSONString()
|
||||
common.OptionMap["TopUpLink"] = common.TopUpLink
|
||||
common.OptionMap["ChatLink"] = common.ChatLink
|
||||
@ -303,6 +304,8 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
err = common.UpdateModelRatioByJSONString(value)
|
||||
case "GroupRatio":
|
||||
err = common.UpdateGroupRatioByJSONString(value)
|
||||
case "UserUsableGroups":
|
||||
err = common.UpdateUserUsableGroupsByJSONString(value)
|
||||
case "CompletionRatio":
|
||||
err = common.UpdateCompletionRatioByJSONString(value)
|
||||
case "ModelPrice":
|
||||
|
@ -25,6 +25,7 @@ type Token struct {
|
||||
ModelLimits string `json:"model_limits" gorm:"type:varchar(1024);default:''"`
|
||||
AllowIps *string `json:"allow_ips" gorm:"default:''"`
|
||||
UsedQuota int `json:"used_quota" gorm:"default:0"` // used quota
|
||||
Group string `json:"group" gorm:"default:''"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
}
|
||||
|
||||
@ -153,7 +154,8 @@ func (token *Token) Insert() error {
|
||||
// Update Make sure your token's fields is completed, because this will update non-zero values
|
||||
func (token *Token) Update() error {
|
||||
var err error
|
||||
err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota", "model_limits_enabled", "model_limits", "allow_ips").Updates(token).Error
|
||||
err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota",
|
||||
"model_limits_enabled", "model_limits", "allow_ips", "group").Updates(token).Error
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,7 @@ func SetApiRouter(router *gin.Engine) {
|
||||
//userRoute.POST("/tokenlog", middleware.CriticalRateLimit(), controller.TokenLog)
|
||||
userRoute.GET("/logout", controller.Logout)
|
||||
userRoute.GET("/epay/notify", controller.EpayNotify)
|
||||
userRoute.GET("/groups", controller.GetUserGroups)
|
||||
|
||||
selfRoute := userRoute.Group("/")
|
||||
selfRoute.Use(middleware.UserAuth())
|
||||
|
@ -23,6 +23,7 @@ const OperationSetting = () => {
|
||||
CompletionRatio: '',
|
||||
ModelPrice: '',
|
||||
GroupRatio: '',
|
||||
UserUsableGroups: '',
|
||||
TopUpLink: '',
|
||||
ChatLink: '',
|
||||
ChatLink2: '', // 添加的新状态变量
|
||||
@ -62,6 +63,7 @@ const OperationSetting = () => {
|
||||
if (
|
||||
item.key === 'ModelRatio' ||
|
||||
item.key === 'GroupRatio' ||
|
||||
item.key === 'UserUsableGroups' ||
|
||||
item.key === 'CompletionRatio' ||
|
||||
item.key === 'ModelPrice'
|
||||
) {
|
||||
|
@ -8,14 +8,14 @@ import {
|
||||
} from '../helpers';
|
||||
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { renderQuota } from '../helpers/render';
|
||||
import {renderGroup, renderQuota} from '../helpers/render';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Form,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Popover,
|
||||
Popover, Space,
|
||||
SplitButtonGroup,
|
||||
Table,
|
||||
Tag,
|
||||
@ -119,7 +119,12 @@ const TokensTable = () => {
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderStatus(text, record.model_limits_enabled)}</div>;
|
||||
return <div>
|
||||
<Space>
|
||||
{renderStatus(text, record.model_limits_enabled)}
|
||||
{renderGroup(record.group)}
|
||||
</Space>
|
||||
</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -15,8 +15,8 @@ export function renderText(text, limit) {
|
||||
export function renderGroup(group) {
|
||||
if (group === '') {
|
||||
return (
|
||||
<Tag size='large' key='default'>
|
||||
unknown
|
||||
<Tag size='large' key='default' color={stringToColor('default')}>
|
||||
default
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
@ -16,7 +16,8 @@ export default function SettingsMagnification(props) {
|
||||
ModelPrice: '',
|
||||
ModelRatio: '',
|
||||
CompletionRatio: '',
|
||||
GroupRatio: ''
|
||||
GroupRatio: '',
|
||||
UserUsableGroups: ''
|
||||
});
|
||||
const refForm = useRef();
|
||||
const [inputsRow, setInputsRow] = useState(inputs);
|
||||
@ -213,6 +214,33 @@ export default function SettingsMagnification(props) {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
<Form.TextArea
|
||||
label={'用户可选分组'}
|
||||
extraText={''}
|
||||
placeholder={'为一个 JSON 文本,键为分组名称,值为倍率'}
|
||||
field={'UserUsableGroups'}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
return verifyJSON(value);
|
||||
},
|
||||
message: '不是合法的 JSON 字符串'
|
||||
}
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
UserUsableGroups: value
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
<Space>
|
||||
|
@ -35,6 +35,7 @@ const EditToken = (props) => {
|
||||
model_limits_enabled: false,
|
||||
model_limits: [],
|
||||
allow_ips: '',
|
||||
group: '',
|
||||
};
|
||||
const [inputs, setInputs] = useState(originInputs);
|
||||
const {
|
||||
@ -44,10 +45,12 @@ const EditToken = (props) => {
|
||||
unlimited_quota,
|
||||
model_limits_enabled,
|
||||
model_limits,
|
||||
allow_ips
|
||||
allow_ips,
|
||||
group
|
||||
} = inputs;
|
||||
// const [visible, setVisible] = useState(false);
|
||||
const [models, setModels] = useState({});
|
||||
const [groups, setGroups] = useState([]);
|
||||
const navigate = useNavigate();
|
||||
const handleInputChange = (name, value) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
@ -88,6 +91,22 @@ const EditToken = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadGroups = async () => {
|
||||
let res = await API.get(`/api/user/groups`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
// return data is a map, key is group name, value is group description
|
||||
// label is group description, value is group name
|
||||
let localGroupOptions = Object.keys(data).map((group) => ({
|
||||
label: data[group],
|
||||
value: group,
|
||||
}));
|
||||
setGroups(localGroupOptions);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const loadToken = async () => {
|
||||
setLoading(true);
|
||||
let res = await API.get(`/api/token/${props.editingToken.id}`);
|
||||
@ -120,6 +139,7 @@ const EditToken = (props) => {
|
||||
});
|
||||
}
|
||||
loadModels();
|
||||
loadGroups();
|
||||
}, [isEdit]);
|
||||
|
||||
// 新增 state 变量 tokenCount 来记录用户想要创建的令牌数量,默认为 1
|
||||
@ -253,150 +273,150 @@ const EditToken = (props) => {
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<Input
|
||||
style={{ marginTop: 20 }}
|
||||
label='名称'
|
||||
name='name'
|
||||
placeholder={'请输入名称'}
|
||||
onChange={(value) => handleInputChange('name', value)}
|
||||
value={name}
|
||||
autoComplete='new-password'
|
||||
required={!isEdit}
|
||||
style={{marginTop: 20}}
|
||||
label='名称'
|
||||
name='name'
|
||||
placeholder={'请输入名称'}
|
||||
onChange={(value) => handleInputChange('name', value)}
|
||||
value={name}
|
||||
autoComplete='new-password'
|
||||
required={!isEdit}
|
||||
/>
|
||||
<Divider />
|
||||
<Divider/>
|
||||
<DatePicker
|
||||
label='过期时间'
|
||||
name='expired_time'
|
||||
placeholder={'请选择过期时间'}
|
||||
onChange={(value) => handleInputChange('expired_time', value)}
|
||||
value={expired_time}
|
||||
autoComplete='new-password'
|
||||
type='dateTime'
|
||||
label='过期时间'
|
||||
name='expired_time'
|
||||
placeholder={'请选择过期时间'}
|
||||
onChange={(value) => handleInputChange('expired_time', value)}
|
||||
value={expired_time}
|
||||
autoComplete='new-password'
|
||||
type='dateTime'
|
||||
/>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<div style={{marginTop: 20}}>
|
||||
<Space>
|
||||
<Button
|
||||
type={'tertiary'}
|
||||
onClick={() => {
|
||||
setExpiredTime(0, 0, 0, 0);
|
||||
}}
|
||||
type={'tertiary'}
|
||||
onClick={() => {
|
||||
setExpiredTime(0, 0, 0, 0);
|
||||
}}
|
||||
>
|
||||
永不过期
|
||||
</Button>
|
||||
<Button
|
||||
type={'tertiary'}
|
||||
onClick={() => {
|
||||
setExpiredTime(0, 0, 1, 0);
|
||||
}}
|
||||
type={'tertiary'}
|
||||
onClick={() => {
|
||||
setExpiredTime(0, 0, 1, 0);
|
||||
}}
|
||||
>
|
||||
一小时
|
||||
</Button>
|
||||
<Button
|
||||
type={'tertiary'}
|
||||
onClick={() => {
|
||||
setExpiredTime(1, 0, 0, 0);
|
||||
}}
|
||||
type={'tertiary'}
|
||||
onClick={() => {
|
||||
setExpiredTime(1, 0, 0, 0);
|
||||
}}
|
||||
>
|
||||
一个月
|
||||
</Button>
|
||||
<Button
|
||||
type={'tertiary'}
|
||||
onClick={() => {
|
||||
setExpiredTime(0, 1, 0, 0);
|
||||
}}
|
||||
type={'tertiary'}
|
||||
onClick={() => {
|
||||
setExpiredTime(0, 1, 0, 0);
|
||||
}}
|
||||
>
|
||||
一天
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
<Divider/>
|
||||
<Banner
|
||||
type={'warning'}
|
||||
description={
|
||||
'注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。'
|
||||
}
|
||||
type={'warning'}
|
||||
description={
|
||||
'注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。'
|
||||
}
|
||||
></Banner>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<div style={{marginTop: 20}}>
|
||||
<Typography.Text>{`额度${renderQuotaWithPrompt(remain_quota)}`}</Typography.Text>
|
||||
</div>
|
||||
<AutoComplete
|
||||
style={{ marginTop: 8 }}
|
||||
name='remain_quota'
|
||||
placeholder={'请输入额度'}
|
||||
onChange={(value) => handleInputChange('remain_quota', value)}
|
||||
value={remain_quota}
|
||||
autoComplete='new-password'
|
||||
type='number'
|
||||
// position={'top'}
|
||||
data={[
|
||||
{ value: 500000, label: '1$' },
|
||||
{ value: 5000000, label: '10$' },
|
||||
{ value: 25000000, label: '50$' },
|
||||
{ value: 50000000, label: '100$' },
|
||||
{ value: 250000000, label: '500$' },
|
||||
{ value: 500000000, label: '1000$' },
|
||||
]}
|
||||
disabled={unlimited_quota}
|
||||
style={{marginTop: 8}}
|
||||
name='remain_quota'
|
||||
placeholder={'请输入额度'}
|
||||
onChange={(value) => handleInputChange('remain_quota', value)}
|
||||
value={remain_quota}
|
||||
autoComplete='new-password'
|
||||
type='number'
|
||||
// position={'top'}
|
||||
data={[
|
||||
{value: 500000, label: '1$'},
|
||||
{value: 5000000, label: '10$'},
|
||||
{value: 25000000, label: '50$'},
|
||||
{value: 50000000, label: '100$'},
|
||||
{value: 250000000, label: '500$'},
|
||||
{value: 500000000, label: '1000$'},
|
||||
]}
|
||||
disabled={unlimited_quota}
|
||||
/>
|
||||
|
||||
{!isEdit && (
|
||||
<>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Typography.Text>新建数量</Typography.Text>
|
||||
</div>
|
||||
<AutoComplete
|
||||
style={{ marginTop: 8 }}
|
||||
label='数量'
|
||||
placeholder={'请选择或输入创建令牌的数量'}
|
||||
onChange={(value) => handleTokenCountChange(value)}
|
||||
onSelect={(value) => handleTokenCountChange(value)}
|
||||
value={tokenCount.toString()}
|
||||
autoComplete='off'
|
||||
type='number'
|
||||
data={[
|
||||
{ value: 10, label: '10个' },
|
||||
{ value: 20, label: '20个' },
|
||||
{ value: 30, label: '30个' },
|
||||
{ value: 100, label: '100个' },
|
||||
]}
|
||||
disabled={unlimited_quota}
|
||||
/>
|
||||
</>
|
||||
<>
|
||||
<div style={{marginTop: 20}}>
|
||||
<Typography.Text>新建数量</Typography.Text>
|
||||
</div>
|
||||
<AutoComplete
|
||||
style={{marginTop: 8}}
|
||||
label='数量'
|
||||
placeholder={'请选择或输入创建令牌的数量'}
|
||||
onChange={(value) => handleTokenCountChange(value)}
|
||||
onSelect={(value) => handleTokenCountChange(value)}
|
||||
value={tokenCount.toString()}
|
||||
autoComplete='off'
|
||||
type='number'
|
||||
data={[
|
||||
{value: 10, label: '10个'},
|
||||
{value: 20, label: '20个'},
|
||||
{value: 30, label: '30个'},
|
||||
{value: 100, label: '100个'},
|
||||
]}
|
||||
disabled={unlimited_quota}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Button
|
||||
style={{ marginTop: 8 }}
|
||||
type={'warning'}
|
||||
onClick={() => {
|
||||
setUnlimitedQuota();
|
||||
}}
|
||||
style={{marginTop: 8}}
|
||||
type={'warning'}
|
||||
onClick={() => {
|
||||
setUnlimitedQuota();
|
||||
}}
|
||||
>
|
||||
{unlimited_quota ? '取消无限额度' : '设为无限额度'}
|
||||
</Button>
|
||||
</div>
|
||||
<Divider />
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Divider/>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text>IP白名单(请勿过度信任此功能)</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
label='IP白名单'
|
||||
name='allow_ips'
|
||||
placeholder={'允许的IP,一行一个'}
|
||||
onChange={(value) => {
|
||||
handleInputChange('allow_ips', value);
|
||||
}}
|
||||
value={inputs.allow_ips}
|
||||
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
label='IP白名单'
|
||||
name='allow_ips'
|
||||
placeholder={'允许的IP,一行一个'}
|
||||
onChange={(value) => {
|
||||
handleInputChange('allow_ips', value);
|
||||
}}
|
||||
value={inputs.allow_ips}
|
||||
style={{fontFamily: 'JetBrains Mono, Consolas'}}
|
||||
/>
|
||||
<div style={{ marginTop: 10, display: 'flex' }}>
|
||||
<div style={{marginTop: 10, display: 'flex'}}>
|
||||
<Space>
|
||||
<Checkbox
|
||||
name='model_limits_enabled'
|
||||
checked={model_limits_enabled}
|
||||
onChange={(e) =>
|
||||
handleInputChange('model_limits_enabled', e.target.checked)
|
||||
}
|
||||
name='model_limits_enabled'
|
||||
checked={model_limits_enabled}
|
||||
onChange={(e) =>
|
||||
handleInputChange('model_limits_enabled', e.target.checked)
|
||||
}
|
||||
></Checkbox>
|
||||
<Typography.Text>
|
||||
启用模型限制(非必要,不建议启用)
|
||||
@ -405,19 +425,36 @@ const EditToken = (props) => {
|
||||
</div>
|
||||
|
||||
<Select
|
||||
style={{ marginTop: 8 }}
|
||||
placeholder={'请选择该渠道所支持的模型'}
|
||||
name='models'
|
||||
required
|
||||
multiple
|
||||
selection
|
||||
onChange={(value) => {
|
||||
handleInputChange('model_limits', value);
|
||||
}}
|
||||
value={inputs.model_limits}
|
||||
autoComplete='new-password'
|
||||
optionList={models}
|
||||
disabled={!model_limits_enabled}
|
||||
style={{marginTop: 8}}
|
||||
placeholder={'请选择该渠道所支持的模型'}
|
||||
name='models'
|
||||
required
|
||||
multiple
|
||||
selection
|
||||
onChange={(value) => {
|
||||
handleInputChange('model_limits', value);
|
||||
}}
|
||||
value={inputs.model_limits}
|
||||
autoComplete='new-password'
|
||||
optionList={models}
|
||||
disabled={!model_limits_enabled}
|
||||
/>
|
||||
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text>令牌分组,不选为默认分组</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
style={{marginTop: 8}}
|
||||
placeholder={'令牌分组,不选为默认分组'}
|
||||
name='gruop'
|
||||
required
|
||||
selection
|
||||
onChange={(value) => {
|
||||
handleInputChange('group', value);
|
||||
}}
|
||||
value={inputs.group}
|
||||
autoComplete='new-password'
|
||||
optionList={groups}
|
||||
/>
|
||||
</Spin>
|
||||
</SideSheet>
|
||||
|
Loading…
Reference in New Issue
Block a user