diff --git a/controller/channel.go b/controller/channel.go
index c05909eb..3c7402d3 100644
--- a/controller/channel.go
+++ b/controller/channel.go
@@ -1,6 +1,7 @@
package controller
import (
+ "errors"
"net/http"
"one-api/common"
"one-api/model"
@@ -11,7 +12,7 @@ import (
)
func GetChannelsList(c *gin.Context) {
- var params model.GenericParams
+ var params model.SearchChannelsParams
if err := c.ShouldBindQuery(¶ms); err != nil {
common.APIRespondWithError(c, http.StatusOK, err)
return
@@ -145,3 +146,54 @@ func UpdateChannel(c *gin.Context) {
"data": channel,
})
}
+
+func BatchUpdateChannelsAzureApi(c *gin.Context) {
+ var params model.BatchChannelsParams
+ err := c.ShouldBindJSON(¶ms)
+ if err != nil {
+ common.APIRespondWithError(c, http.StatusOK, err)
+ return
+ }
+
+ if params.Ids == nil || len(params.Ids) == 0 {
+ common.APIRespondWithError(c, http.StatusOK, errors.New("ids不能为空"))
+ return
+ }
+ var count int64
+ count, err = model.BatchUpdateChannelsAzureApi(¶ms)
+ if err != nil {
+ common.APIRespondWithError(c, http.StatusOK, err)
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "data": count,
+ "success": true,
+ "message": "更新成功",
+ })
+}
+
+func BatchDelModelChannels(c *gin.Context) {
+ var params model.BatchChannelsParams
+ err := c.ShouldBindJSON(¶ms)
+ if err != nil {
+ common.APIRespondWithError(c, http.StatusOK, err)
+ return
+ }
+
+ if params.Ids == nil || len(params.Ids) == 0 {
+ common.APIRespondWithError(c, http.StatusOK, errors.New("ids不能为空"))
+ return
+ }
+
+ var count int64
+ count, err = model.BatchDelModelChannels(¶ms)
+ if err != nil {
+ common.APIRespondWithError(c, http.StatusOK, err)
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "data": count,
+ "success": true,
+ "message": "更新成功",
+ })
+}
diff --git a/model/channel.go b/model/channel.go
index 548a8c96..608e67ad 100644
--- a/model/channel.go
+++ b/model/channel.go
@@ -2,31 +2,32 @@ package model
import (
"one-api/common"
+ "strings"
"gorm.io/gorm"
)
type Channel struct {
Id int `json:"id"`
- Type int `json:"type" gorm:"default:0"`
- Key string `json:"key" gorm:"type:varchar(767);not null;index"`
- Status int `json:"status" gorm:"default:1"`
- Name string `json:"name" gorm:"index"`
+ Type int `json:"type" form:"type" gorm:"default:0"`
+ Key string `json:"key" form:"key" gorm:"type:varchar(767);not null;index"`
+ Status int `json:"status" form:"status" gorm:"default:1"`
+ Name string `json:"name" form:"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"`
+ Other string `json:"other" form:"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(32);default:'default'"`
+ Models string `json:"models" form:"models"`
+ Group string `json:"group" form:"group" gorm:"type:varchar(32);default:'default'"`
UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"`
ModelMapping *string `json:"model_mapping" gorm:"type:varchar(1024);default:''"`
Priority *int64 `json:"priority" gorm:"bigint;default:0"`
Proxy *string `json:"proxy" gorm:"type:varchar(255);default:''"`
- TestModel string `json:"test_model" gorm:"type:varchar(50);default:''"`
+ TestModel string `json:"test_model" form:"test_model" gorm:"type:varchar(50);default:''"`
}
var allowedChannelOrderFields = map[string]bool{
@@ -40,16 +41,46 @@ var allowedChannelOrderFields = map[string]bool{
"priority": true,
}
-func GetChannelsList(params *GenericParams) (*DataResult[Channel], error) {
+type SearchChannelsParams struct {
+ Channel
+ PaginationParams
+}
+
+func GetChannelsList(params *SearchChannelsParams) (*DataResult[Channel], error) {
var channels []*Channel
db := DB.Omit("key")
- if params.Keyword != "" {
- keyCol := "`key`"
- if common.UsingPostgreSQL {
- keyCol = `"key"`
- }
- db = db.Where("id = ? or name LIKE ? or "+keyCol+" = ?", common.String2Int(params.Keyword), params.Keyword+"%", params.Keyword)
+
+ if params.Type != 0 {
+ db = db.Where("type = ?", params.Type)
+ }
+
+ if params.Status != 0 {
+ db = db.Where("status = ?", params.Status)
+ }
+
+ if params.Name != "" {
+ db = db.Where("name LIKE ?", params.Name+"%")
+ }
+
+ if params.Group != "" {
+ db = db.Where("id IN (SELECT channel_id FROM abilities WHERE "+quotePostgresField("group")+" = ?)", params.Group)
+ }
+
+ if params.Models != "" {
+ db = db.Where("id IN (SELECT channel_id FROM abilities WHERE model IN (?))", params.Models)
+ }
+
+ if params.Other != "" {
+ db = db.Where("other LIKE ?", params.Other+"%")
+ }
+
+ if params.Key != "" {
+ db = db.Where(quotePostgresField("key")+" = ?", params.Key)
+ }
+
+ if params.TestModel != "" {
+ db = db.Where("test_model LIKE ?", params.TestModel+"%")
}
return PaginateAndOrder[Channel](db, ¶ms.PaginationParams, &channels, allowedChannelOrderFields)
@@ -87,6 +118,45 @@ func BatchInsertChannels(channels []Channel) error {
return nil
}
+type BatchChannelsParams struct {
+ Value string `json:"value" form:"value" binding:"required"`
+ Ids []int `json:"ids" form:"ids" binding:"required"`
+}
+
+func BatchUpdateChannelsAzureApi(params *BatchChannelsParams) (int64, error) {
+ db := DB.Model(&Channel{}).Where("id IN ?", params.Ids).Update("other", params.Value)
+ if db.Error != nil {
+ return 0, db.Error
+ }
+ return db.RowsAffected, nil
+}
+
+func BatchDelModelChannels(params *BatchChannelsParams) (int64, error) {
+ var count int64
+
+ var channels []*Channel
+ err := DB.Select("id, models, "+quotePostgresField("group")).Find(&channels, "id IN ?", params.Ids).Error
+ if err != nil {
+ return 0, err
+ }
+
+ for _, channel := range channels {
+ modelsSlice := strings.Split(channel.Models, ",")
+ for i, m := range modelsSlice {
+ if m == params.Value {
+ modelsSlice = append(modelsSlice[:i], modelsSlice[i+1:]...)
+ break
+ }
+ }
+
+ channel.Models = strings.Join(modelsSlice, ",")
+ channel.Update()
+ count++
+ }
+
+ return count, nil
+}
+
func (channel *Channel) GetPriority() int64 {
if channel.Priority == nil {
return 0
diff --git a/model/common.go b/model/common.go
index 84e1e422..018a8642 100644
--- a/model/common.go
+++ b/model/common.go
@@ -120,3 +120,23 @@ func getTimestampGroupsSelect(fieldName, groupType, alias string) string {
return groupSelect
}
+
+func quotePostgresField(field string) string {
+ if common.UsingPostgreSQL {
+ return fmt.Sprintf(`"%s"`, field)
+ }
+
+ return fmt.Sprintf("`%s`", field)
+}
+
+func assembleSumSelectStr(selectStr string) string {
+ sumSelectStr := "%s(sum(%s),0)"
+ nullfunc := "ifnull"
+ if common.UsingPostgreSQL {
+ nullfunc = "coalesce"
+ }
+
+ sumSelectStr = fmt.Sprintf(sumSelectStr, nullfunc, selectStr)
+
+ return sumSelectStr
+}
diff --git a/model/log.go b/model/log.go
index a3cc46ad..988a2d59 100644
--- a/model/log.go
+++ b/model/log.go
@@ -269,15 +269,3 @@ func GetChannelExpensesByPeriod(startTimestamp, endTimestamp int64) (LogStatisti
return LogStatistics, err
}
-
-func assembleSumSelectStr(selectStr string) string {
- sumSelectStr := "%s(sum(%s),0)"
- nullfunc := "ifnull"
- if common.UsingPostgreSQL {
- nullfunc = "coalesce"
- }
-
- sumSelectStr = fmt.Sprintf(sumSelectStr, nullfunc, selectStr)
-
- return sumSelectStr
-}
diff --git a/router/api-router.go b/router/api-router.go
index f1bd65f2..a5ea2e5b 100644
--- a/router/api-router.go
+++ b/router/api-router.go
@@ -81,6 +81,8 @@ func SetApiRouter(router *gin.Engine) {
channelRoute.GET("/update_balance/:id", controller.UpdateChannelBalance)
channelRoute.POST("/", controller.AddChannel)
channelRoute.PUT("/", controller.UpdateChannel)
+ channelRoute.PUT("/batch/azure_api", controller.BatchUpdateChannelsAzureApi)
+ channelRoute.PUT("/batch/del_model", controller.BatchDelModelChannels)
channelRoute.DELETE("/disabled", controller.DeleteDisabledChannel)
channelRoute.DELETE("/:id", controller.DeleteChannel)
}
diff --git a/web/src/ui-component/AdminContainer.js b/web/src/ui-component/AdminContainer.js
index eff42a22..85a131e8 100644
--- a/web/src/ui-component/AdminContainer.js
+++ b/web/src/ui-component/AdminContainer.js
@@ -1,11 +1,9 @@
import { styled } from '@mui/material/styles';
import { Container } from '@mui/material';
-const AdminContainer = styled(Container)(({ theme }) => ({
- [theme.breakpoints.down('md')]: {
- paddingLeft: '0px',
- paddingRight: '0px'
- }
-}));
+const AdminContainer = styled(Container)({
+ paddingLeft: '0px !important',
+ paddingRight: '0px !important'
+});
export default AdminContainer;
diff --git a/web/src/views/Channel/component/BatchAzureAPI.js b/web/src/views/Channel/component/BatchAzureAPI.js
new file mode 100644
index 00000000..d5dbed94
--- /dev/null
+++ b/web/src/views/Channel/component/BatchAzureAPI.js
@@ -0,0 +1,125 @@
+import { useState } from 'react';
+import { Grid, TextField, InputAdornment, Checkbox, Button, FormControlLabel, IconButton } from '@mui/material';
+import { gridSpacing } from 'store/constant';
+import { IconSearch, IconSend } from '@tabler/icons-react';
+import { fetchChannelData } from '../index';
+import { API } from 'utils/api';
+import { showError, showSuccess } from 'utils/common';
+
+const BatchAzureAPI = () => {
+ const [value, setValue] = useState('');
+ const [data, setData] = useState([]);
+ const [selected, setSelected] = useState([]);
+ const [replaceValue, setReplaceValue] = useState('');
+
+ const handleSearch = async () => {
+ const data = await fetchChannelData(0, 100, { other: value, type: 3 }, 'desc', 'id');
+ if (data) {
+ setData(data.data);
+ }
+ };
+
+ const handleSelect = (id) => {
+ setSelected((prev) => {
+ if (prev.includes(id)) {
+ return prev.filter((i) => i !== id);
+ } else {
+ return [...prev, id];
+ }
+ });
+ };
+
+ const handleSelectAll = () => {
+ if (selected.length === data.length) {
+ setSelected([]);
+ } else {
+ setSelected(data.map((item) => item.id));
+ }
+ };
+
+ const handleSubmit = async () => {
+ try {
+ const res = await API.put(`/api/channel/batch/azure_api`, {
+ ids: selected,
+ value: replaceValue
+ });
+
+ const { success, message, data } = res.data;
+ if (success) {
+ showSuccess('成功更新' + data + '条数据');
+ return;
+ } else {
+ showError(message);
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ return (
+
+
+ {
+ setValue(e.target.value);
+ }}
+ InputProps={{
+ endAdornment: (
+
+
+
+
+
+ )
+ }}
+ />
+
+ {data.length === 0 ? (
+
+ 暂无数据
+
+ ) : (
+ <>
+
+
+
+
+ {data.map((item) => (
+ handleSelect(item.id)} />}
+ label={item.name + '(' + item.other + ')'}
+ />
+ ))}
+
+
+ {
+ setReplaceValue(e.target.value);
+ }}
+ InputProps={{
+ endAdornment: (
+
+
+
+
+
+ )
+ }}
+ />
+
+ >
+ )}
+
+ );
+};
+
+export default BatchAzureAPI;
diff --git a/web/src/views/Channel/component/BatchDelModel.js b/web/src/views/Channel/component/BatchDelModel.js
new file mode 100644
index 00000000..9d57f185
--- /dev/null
+++ b/web/src/views/Channel/component/BatchDelModel.js
@@ -0,0 +1,125 @@
+import { useState } from 'react';
+import { Grid, TextField, InputAdornment, Checkbox, Button, FormControlLabel, IconButton, Alert } from '@mui/material';
+import { gridSpacing } from 'store/constant';
+import { IconSearch, IconHttpDelete } from '@tabler/icons-react';
+import { fetchChannelData } from '../index';
+import { API } from 'utils/api';
+import { showError, showSuccess } from 'utils/common';
+
+const BatchDelModel = () => {
+ const [value, setValue] = useState('');
+ const [data, setData] = useState([]);
+ const [selected, setSelected] = useState([]);
+ const [loadding, setLoadding] = useState(false);
+
+ const handleSearch = async () => {
+ const data = await fetchChannelData(0, 100, { models: value }, 'desc', 'id');
+ if (data) {
+ // 遍历data 逗号分隔models, 检测是否只有一个model 如果是则排除
+ const newData = data.data.filter((item) => {
+ if (item.models.split(',').length > 1) {
+ return true;
+ }
+ return false;
+ });
+
+ setData(newData);
+ }
+ };
+
+ const handleSelect = (id) => {
+ setSelected((prev) => {
+ if (prev.includes(id)) {
+ return prev.filter((i) => i !== id);
+ } else {
+ return [...prev, id];
+ }
+ });
+ };
+
+ const handleSelectAll = () => {
+ if (selected.length === data.length) {
+ setSelected([]);
+ } else {
+ setSelected(data.map((item) => item.id));
+ }
+ };
+
+ const handleSubmit = async () => {
+ if (value === '' || selected.length === 0) {
+ return;
+ }
+ setLoadding(true);
+ try {
+ const res = await API.put(`/api/channel/batch/del_model`, {
+ ids: selected,
+ value: value
+ });
+
+ const { success, message, data } = res.data;
+ if (success) {
+ showSuccess('成功删除' + data + '条数据');
+ } else {
+ showError(message);
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ setLoadding(false);
+ };
+
+ return (
+
+
+ 如果渠道只有一个模型的,将不会显示,请手动去列表删除渠道
+
+
+ {
+ setValue(e.target.value);
+ }}
+ InputProps={{
+ endAdornment: (
+
+
+
+
+
+ )
+ }}
+ />
+
+ {data.length === 0 ? (
+
+ 暂无数据
+
+ ) : (
+ <>
+
+
+
+
+ {data.map((item) => (
+ handleSelect(item.id)} />}
+ label={item.name}
+ />
+ ))}
+
+
+ } onClick={handleSubmit} disabled={loadding}>
+ 删除
+
+
+ >
+ )}
+
+ );
+};
+
+export default BatchDelModel;
diff --git a/web/src/views/Channel/component/BatchModal.js b/web/src/views/Channel/component/BatchModal.js
new file mode 100644
index 00000000..43ac6a1c
--- /dev/null
+++ b/web/src/views/Channel/component/BatchModal.js
@@ -0,0 +1,67 @@
+import PropTypes from 'prop-types';
+import { useState } from 'react';
+import { Dialog, DialogTitle, DialogContent, DialogActions, Divider, Button, Tabs, Tab, Box } from '@mui/material';
+import BatchAzureAPI from './BatchAzureAPI';
+import BatchDelModel from './BatchDelModel';
+
+function CustomTabPanel(props) {
+ const { children, value, index, ...other } = props;
+
+ return (
+
+ {value === index && {children}}
+
+ );
+}
+
+CustomTabPanel.propTypes = {
+ children: PropTypes.node,
+ index: PropTypes.number.isRequired,
+ value: PropTypes.number.isRequired
+};
+
+function a11yProps(index) {
+ return {
+ id: `channel-tab-${index}`,
+ 'aria-controls': `channel-tabpanel-${index}`
+ };
+}
+
+const BatchModal = ({ open, setOpen }) => {
+ const [value, setValue] = useState(0);
+ const handleChange = (event, newValue) => {
+ setValue(newValue);
+ };
+
+ return (
+
+ );
+};
+
+export default BatchModal;
+
+BatchModal.propTypes = {
+ open: PropTypes.bool,
+ setOpen: PropTypes.func
+};
diff --git a/web/src/views/Channel/component/EditModal.js b/web/src/views/Channel/component/EditModal.js
index 379746df..a1fb429c 100644
--- a/web/src/views/Channel/component/EditModal.js
+++ b/web/src/views/Channel/component/EditModal.js
@@ -66,13 +66,12 @@ const validationSchema = Yup.object().shape({
})
});
-const EditModal = ({ open, channelId, onCancel, onOk }) => {
+const EditModal = ({ open, channelId, onCancel, onOk, groupOptions }) => {
const theme = useTheme();
// const [loading, setLoading] = useState(false);
const [initialInput, setInitialInput] = useState(defaultConfig.input);
const [inputLabel, setInputLabel] = useState(defaultConfig.inputLabel); //
const [inputPrompt, setInputPrompt] = useState(defaultConfig.prompt);
- const [groupOptions, setGroupOptions] = useState([]);
const [modelOptions, setModelOptions] = useState([]);
const initChannel = (typeValue) => {
@@ -123,15 +122,6 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => {
return modelList;
};
- const fetchGroups = async () => {
- try {
- let res = await API.get(`/api/group/`);
- setGroupOptions(res.data.data);
- } catch (error) {
- showError(error.message);
- }
- };
-
const fetchModels = async () => {
try {
let res = await API.get(`/api/channel/models`);
@@ -252,7 +242,6 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => {
};
useEffect(() => {
- fetchGroups().then();
fetchModels().then();
}, []);
@@ -596,5 +585,6 @@ EditModal.propTypes = {
open: PropTypes.bool,
channelId: PropTypes.number,
onCancel: PropTypes.func,
- onOk: PropTypes.func
+ onOk: PropTypes.func,
+ groupOptions: PropTypes.array
};
diff --git a/web/src/views/Channel/component/TableRow.js b/web/src/views/Channel/component/TableRow.js
index 72b62c0f..b5765ea6 100644
--- a/web/src/views/Channel/component/TableRow.js
+++ b/web/src/views/Channel/component/TableRow.js
@@ -21,7 +21,11 @@ import {
DialogContentText,
DialogTitle,
Tooltip,
- Button
+ Button,
+ Grid,
+ Collapse,
+ Typography,
+ Box
} from '@mui/material';
import Label from 'ui-component/Label';
@@ -29,9 +33,11 @@ import TableSwitch from 'ui-component/Switch';
import ResponseTimeLabel from './ResponseTimeLabel';
import GroupLabel from './GroupLabel';
-import NameLabel from './NameLabel';
import { IconDotsVertical, IconEdit, IconTrash, IconPencil } from '@tabler/icons-react';
+import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
+import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
+import { copy } from 'utils/common';
export default function ChannelTableRow({ item, manageChannel, handleOpenModal, setModalChannelId }) {
const [open, setOpen] = useState(null);
@@ -41,6 +47,11 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
const [responseTimeData, setResponseTimeData] = useState({ test_time: item.test_time, response_time: item.response_time });
const [itemBalance, setItemBalance] = useState(item.balance);
+ const [openRow, setOpenRow] = useState(false);
+ let modelMap = [];
+ modelMap = item.models.split(',');
+ modelMap.sort();
+
const handleDeleteOpen = () => {
handleCloseMenu();
setOpenDelete(true);
@@ -105,11 +116,15 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
return (
<>
+
+ setOpenRow(!openRow)}>
+ {openRow ? : }
+
+
+
{item.id}
-
-
-
+ {item.name}
@@ -126,7 +141,6 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
)}
-
@@ -196,6 +210,81 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
+
+
+
+
+
+
+
+ 可用模型:
+
+ {modelMap.map((model) => (
+
+ ))}
+
+
+ {item.test_model && (
+
+
+
+ 测速模型:
+
+
+
+
+ )}
+ {item.proxy && (
+
+
+
+ 代理地址:
+
+ {item.proxy}
+
+
+ )}
+ {item.other && (
+
+
+
+ 其他参数:
+
+
+
+
+ )}
+
+
+
+