feat: publish v1.0.0-rc.36-custom.1
This commit is contained in:
commit
3a7e8f3b54
2381 files changed
+551494
No files matched your search
@@ -0,0 +1,337 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetAllModelsMeta 获取模型列表(分页)
|
||||
func GetAllModelsMeta(c *gin.Context) {
|
||||
listModelsMeta(c, "", "")
|
||||
}
|
||||
|
||||
// SearchModelsMeta 搜索模型列表
|
||||
func SearchModelsMeta(c *gin.Context) {
|
||||
listModelsMeta(c, c.Query("keyword"), c.Query("vendor"))
|
||||
}
|
||||
|
||||
func listModelsMeta(c *gin.Context, keyword, vendor string) {
|
||||
squareState := model.ModelSquareState(c.Query("square_state"))
|
||||
switch squareState {
|
||||
case "", model.ModelSquareVisible, model.ModelSquareUnavailable, model.ModelSquareHidden, model.ModelSquarePartial:
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid model square state"})
|
||||
return
|
||||
}
|
||||
|
||||
pageInfo := common.GetPageQuery(c)
|
||||
if squareState != "" && (pageInfo.GetPage() < 1 || pageInfo.GetPageSize() < 1) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Invalid pagination"})
|
||||
return
|
||||
}
|
||||
offset, limit := pageInfo.GetStartIdx(), pageInfo.GetPageSize()
|
||||
if squareState != "" {
|
||||
// Visibility depends on live channels and metadata rules. Filter the
|
||||
// enriched candidate set before counting and paginating the results.
|
||||
offset, limit = 0, -1
|
||||
}
|
||||
search := model.SearchModels
|
||||
if c.Query("include_channel_models") == "true" {
|
||||
search = model.SearchModelsWithChannels
|
||||
}
|
||||
modelsMeta, total, err := search(keyword, vendor, c.Query("status"), c.Query("sync_official"), offset, limit)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if err := enrichModels(modelsMeta); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if squareState != "" {
|
||||
filtered := make([]*model.Model, 0, len(modelsMeta))
|
||||
for _, metadata := range modelsMeta {
|
||||
if metadata.SquareState == squareState {
|
||||
filtered = append(filtered, metadata)
|
||||
}
|
||||
}
|
||||
total = int64(len(filtered))
|
||||
start := len(filtered)
|
||||
if pageInfo.GetPage()-1 <= len(filtered)/pageInfo.GetPageSize() {
|
||||
start = (pageInfo.GetPage() - 1) * pageInfo.GetPageSize()
|
||||
}
|
||||
end := min(start+pageInfo.GetPageSize(), len(filtered))
|
||||
modelsMeta = filtered[start:end]
|
||||
}
|
||||
|
||||
vendorCounts, _ := model.GetVendorModelCounts()
|
||||
pageInfo.SetTotal(int(total))
|
||||
pageInfo.SetItems(modelsMeta)
|
||||
common.ApiSuccess(c, gin.H{
|
||||
"items": modelsMeta,
|
||||
"total": total,
|
||||
"page": pageInfo.GetPage(),
|
||||
"page_size": pageInfo.GetPageSize(),
|
||||
"vendor_counts": vendorCounts,
|
||||
})
|
||||
}
|
||||
|
||||
// GetModelMeta 根据 ID 获取单条模型信息
|
||||
func GetModelMeta(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
var m model.Model
|
||||
if err := model.DB.First(&m, id).Error; err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if err := enrichModels([]*model.Model{&m}); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, &m)
|
||||
}
|
||||
|
||||
// CreateModelMeta 新建模型
|
||||
func CreateModelMeta(c *gin.Context) {
|
||||
var m model.Model
|
||||
if err := c.ShouldBindJSON(&m); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if m.ModelName == "" {
|
||||
common.ApiErrorMsg(c, "模型名称不能为空")
|
||||
return
|
||||
}
|
||||
if err := model.ValidateMetadataValues(model.MetadataValues{Endpoints: m.Endpoints, Status: m.Status, NameRule: m.NameRule}); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
// 名称冲突检查
|
||||
if dup, err := model.IsModelNameDuplicated(0, m.ModelName); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
} else if dup {
|
||||
common.ApiErrorMsg(c, "模型名称已存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.Insert(); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
model.RefreshPricing()
|
||||
m.HasMetadata = m.Id > 0
|
||||
common.ApiSuccess(c, &m)
|
||||
}
|
||||
|
||||
// UpdateModelMeta 更新模型
|
||||
func UpdateModelMeta(c *gin.Context) {
|
||||
statusOnly := c.Query("status_only") == "true"
|
||||
|
||||
var m model.Model
|
||||
if err := c.ShouldBindJSON(&m); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if m.Id == 0 {
|
||||
common.ApiErrorMsg(c, "缺少模型 ID")
|
||||
return
|
||||
}
|
||||
|
||||
if statusOnly {
|
||||
if m.Status != 0 && m.Status != 1 {
|
||||
common.ApiErrorMsg(c, "invalid catalog visibility")
|
||||
return
|
||||
}
|
||||
// 只更新状态,防止误清空其他字段
|
||||
if err := model.DB.Model(&model.Model{}).Where("id = ?", m.Id).Update("status", m.Status).Error; err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if strings.TrimSpace(m.ModelName) == "" {
|
||||
common.ApiErrorMsg(c, "模型名称不能为空")
|
||||
return
|
||||
}
|
||||
if err := model.ValidateMetadataValues(model.MetadataValues{Endpoints: m.Endpoints, Status: m.Status, NameRule: m.NameRule}); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
// 名称冲突检查
|
||||
if dup, err := model.IsModelNameDuplicated(m.Id, m.ModelName); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
} else if dup {
|
||||
common.ApiErrorMsg(c, "模型名称已存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.Update(); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
model.RefreshPricing()
|
||||
m.HasMetadata = m.Id > 0
|
||||
common.ApiSuccess(c, &m)
|
||||
}
|
||||
|
||||
// DeleteModelMeta 删除模型
|
||||
func DeleteModelMeta(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
removeFromChannels, err := strconv.ParseBool(c.DefaultQuery("remove_from_channels", "false"))
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
removePricing, err := strconv.ParseBool(c.DefaultQuery("remove_pricing", "false"))
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if removePricing && c.GetInt("role") != common.RoleRootUser {
|
||||
c.JSON(http.StatusForbidden, gin.H{"success": false, "message": "Model pricing is managed by a super administrator."})
|
||||
return
|
||||
}
|
||||
result, err := model.DeleteModelMetadata([]int{id}, removeFromChannels, removePricing)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
recordManageAudit(c, "model.delete", map[string]any{"model_ids": []int{id}, "remove_from_channels": removeFromChannels, "remove_pricing": removePricing, "updated_channels": result.UpdatedChannels})
|
||||
common.ApiSuccess(c, result)
|
||||
}
|
||||
|
||||
func BatchDeleteModelMeta(c *gin.Context) {
|
||||
var request struct {
|
||||
ModelIDs []int `json:"model_ids"`
|
||||
RemoveFromChannels bool `json:"remove_from_channels"`
|
||||
RemovePricing bool `json:"remove_pricing"`
|
||||
}
|
||||
if err := common.DecodeJson(c.Request.Body, &request); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if request.RemovePricing && c.GetInt("role") != common.RoleRootUser {
|
||||
c.JSON(http.StatusForbidden, gin.H{"success": false, "message": "Model pricing is managed by a super administrator."})
|
||||
return
|
||||
}
|
||||
result, err := model.DeleteModelMetadata(request.ModelIDs, request.RemoveFromChannels, request.RemovePricing)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
recordManageAudit(c, "model.delete_batch", map[string]any{"model_ids": request.ModelIDs, "remove_from_channels": request.RemoveFromChannels, "remove_pricing": request.RemovePricing, "updated_channels": result.UpdatedChannels})
|
||||
common.ApiSuccess(c, result)
|
||||
}
|
||||
|
||||
// enrichModels keeps configured endpoints intact and derives connections from
|
||||
// enabled routes, including hidden or unpriced models absent from the catalog.
|
||||
func enrichModels(models []*model.Model) error {
|
||||
if len(models) == 0 {
|
||||
return nil
|
||||
}
|
||||
configured, err := model.GetConfiguredModelChannels()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, metadata := range models {
|
||||
if metadata == nil {
|
||||
continue
|
||||
}
|
||||
metadata.HasMetadata = metadata.Id > 0
|
||||
channelIDs := make(map[int]struct{})
|
||||
for name, ids := range configured {
|
||||
if metadata.MatchesName(name) {
|
||||
for _, id := range ids {
|
||||
channelIDs[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
metadata.ConfiguredChannelCount = len(channelIDs)
|
||||
}
|
||||
connections, err := model.GetModelConnections()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := model.FillModelSquareStates(models, configured, connections); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, metadata := range models {
|
||||
if metadata == nil {
|
||||
continue
|
||||
}
|
||||
channels := make(map[int]model.BoundChannel)
|
||||
groups := make(map[string]bool)
|
||||
names := make(map[string]bool)
|
||||
endpoints := make(map[string]bool)
|
||||
quotas := make(map[int]bool)
|
||||
for _, connection := range connections {
|
||||
name := connection.Model
|
||||
if !metadata.MatchesName(name) {
|
||||
continue
|
||||
}
|
||||
names[name] = true
|
||||
groups[connection.Group] = true
|
||||
channels[connection.ChannelId] = model.BoundChannel{Name: connection.ChannelName, Type: connection.ChannelType}
|
||||
for _, endpoint := range model.GetModelSupportEndpointTypes(name) {
|
||||
endpoints[string(endpoint)] = true
|
||||
}
|
||||
for _, quota := range model.GetModelQuotaTypes(name) {
|
||||
quotas[quota] = true
|
||||
}
|
||||
}
|
||||
metadata.BoundChannels = nil
|
||||
metadata.EnableGroups = nil
|
||||
metadata.SupportedEndpoints = nil
|
||||
metadata.QuotaTypes = nil
|
||||
metadata.MatchedModels = nil
|
||||
for _, channel := range channels {
|
||||
metadata.BoundChannels = append(metadata.BoundChannels, channel)
|
||||
}
|
||||
sort.Slice(metadata.BoundChannels, func(i, j int) bool {
|
||||
a, b := metadata.BoundChannels[i], metadata.BoundChannels[j]
|
||||
if a.Name == b.Name {
|
||||
return a.Type < b.Type
|
||||
}
|
||||
return a.Name < b.Name
|
||||
})
|
||||
for group := range groups {
|
||||
metadata.EnableGroups = append(metadata.EnableGroups, group)
|
||||
}
|
||||
for endpoint := range endpoints {
|
||||
metadata.SupportedEndpoints = append(metadata.SupportedEndpoints, endpoint)
|
||||
}
|
||||
for quota := range quotas {
|
||||
metadata.QuotaTypes = append(metadata.QuotaTypes, quota)
|
||||
}
|
||||
sort.Strings(metadata.EnableGroups)
|
||||
sort.Strings(metadata.SupportedEndpoints)
|
||||
sort.Ints(metadata.QuotaTypes)
|
||||
if metadata.NameRule != model.NameRuleExact {
|
||||
for name := range names {
|
||||
metadata.MatchedModels = append(metadata.MatchedModels, name)
|
||||
}
|
||||
sort.Strings(metadata.MatchedModels)
|
||||
metadata.MatchedCount = len(names)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user