Files
new-api-custom/model/vendor_management.go
T
Codex bb7968b612
Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 9m20s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 55m7s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
feat: publish v1.0.0-rc.36-custom.1
2026-09-11 05:07:48 +08:00

249 lines
7.9 KiB
Go

package model
import (
"crypto/sha256"
"errors"
"fmt"
"slices"
"strings"
"unicode/utf8"
"github.com/QuantumNous/new-api/common"
"gorm.io/gorm"
)
var ErrVendorConflict = errors.New("vendor data changed; preview again before applying")
// VendorReferenceError reports every referenced vendor in a rejected delete.
type VendorReferenceError struct {
Counts map[int]int64 `json:"reference_counts"`
}
func (e *VendorReferenceError) Error() string {
return "vendors are still referenced by models; transfer or clear their assignments first"
}
func FindMetadataVendor(vendors map[string]*Vendor, name string) *Vendor {
for _, vendor := range vendors {
if strings.EqualFold(strings.TrimSpace(vendor.Name), strings.TrimSpace(name)) {
return vendor
}
}
return nil
}
func validateModelVendor(tx *gorm.DB, id int) error {
if id == 0 {
return nil
}
if id < 0 {
return errors.New("select a saved vendor")
}
var count int64
if err := tx.Model(&Vendor{}).Where("id = ?", id).Count(&count).Error; err != nil {
return err
}
if count == 0 {
return errors.New("vendor does not exist")
}
return nil
}
func validateVendorMetadata(tx *gorm.DB, vendor *Vendor) error {
vendor.Name = strings.TrimSpace(vendor.Name)
vendor.Icon = strings.TrimSpace(vendor.Icon)
if vendor.Name == "" {
return errors.New("vendor name is required")
}
if utf8.RuneCountInString(vendor.Name) > 128 || utf8.RuneCountInString(vendor.Icon) > 128 {
return errors.New("vendor name and icon must not exceed 128 characters")
}
var others []Vendor
if err := tx.Select("id", "name").Where("id <> ?", vendor.Id).Find(&others).Error; err != nil {
return err
}
for _, other := range others {
if strings.EqualFold(strings.TrimSpace(other.Name), vendor.Name) {
return errors.New("vendor name already exists")
}
}
return nil
}
func VendorRecordVersion(v *Vendor) string {
encoded, _ := common.Marshal([]any{v.Id, v.Name, v.Description, v.Icon, v.Status, v.CreatedTime, v.UpdatedTime})
return fmt.Sprintf("%x", sha256.Sum256(encoded))
}
// VendorOperation uses explicit selections for both preview and application.
type VendorOperation struct {
Action string `json:"action"`
VendorIDs []int `json:"vendor_ids"`
ModelIDs []int `json:"model_ids"`
TargetVendorID int `json:"target_vendor_id"`
ExpectedVersion string `json:"expected_version,omitempty"`
}
type VendorAssignmentModel struct {
ID int `json:"id"`
ModelName string `json:"model_name"`
NameRule int `json:"name_rule"`
VendorID int `json:"vendor_id"`
VendorName string `json:"vendor_name"`
UpdatedTime int64 `json:"updated_time"`
}
type VendorOperationPreview struct {
Action string `json:"action"`
Sources []Vendor `json:"sources"`
Target *Vendor `json:"target"`
Models []VendorAssignmentModel `json:"models"`
Version string `json:"version"`
}
type VendorOperationResult struct {
UpdatedModels []int `json:"updated_models"`
DeletedVendors []int `json:"deleted_vendors"`
}
func buildVendorOperationPreview(db *gorm.DB, operation VendorOperation) (*VendorOperationPreview, error) {
if operation.Action != "assign" && operation.Action != "merge" && operation.Action != "delete" {
return nil, errors.New("unsupported vendor operation")
}
ids := append([]int{}, operation.VendorIDs...)
if operation.Action == "assign" {
ids = append([]int{}, operation.ModelIDs...)
}
if len(ids) == 0 || len(ids) > 1000 {
return nil, errors.New("select between 1 and 1000 records")
}
slices.Sort(ids)
for i, id := range ids {
if id <= 0 || i > 0 && ids[i-1] == id {
return nil, errors.New("invalid or duplicate selection")
}
}
preview := &VendorOperationPreview{Action: operation.Action, Sources: []Vendor{}, Models: []VendorAssignmentModel{}}
var vendors []Vendor
if err := db.Session(&gorm.Session{}).Order("id").Find(&vendors).Error; err != nil {
return nil, err
}
byID := make(map[int]*Vendor, len(vendors))
for i := range vendors {
byID[vendors[i].Id] = &vendors[i]
}
if operation.Action != "delete" {
if operation.TargetVendorID < 0 || operation.Action == "merge" && operation.TargetVendorID == 0 {
return nil, errors.New("select a saved target vendor")
}
if operation.TargetVendorID != 0 {
preview.Target = byID[operation.TargetVendorID]
if preview.Target == nil {
return nil, fmt.Errorf("%w: target vendor does not exist", ErrVendorConflict)
}
}
}
var models []Model
query := db.Session(&gorm.Session{}).Model(&Model{}).Order("id")
if operation.Action == "assign" {
query = query.Where("id IN ?", ids)
} else {
for _, id := range ids {
vendor := byID[id]
if vendor == nil {
return nil, fmt.Errorf("%w: source vendor does not exist", ErrVendorConflict)
}
if operation.Action == "merge" && id == operation.TargetVendorID {
return nil, errors.New("target vendor cannot also be a source")
}
preview.Sources = append(preview.Sources, *vendor)
}
query = query.Where("vendor_id IN ?", ids)
}
if err := query.Find(&models).Error; err != nil {
return nil, err
}
if operation.Action == "assign" && len(models) != len(ids) {
return nil, fmt.Errorf("%w: a selected model no longer exists", ErrVendorConflict)
}
seen := make(map[int]bool)
modelVersions := make([]string, 0, len(models))
counts := make(map[int]int64)
for _, item := range models {
modelVersions = append(modelVersions, MetadataRecordVersion(&item, nil, nil))
row := VendorAssignmentModel{ID: item.Id, ModelName: item.ModelName, NameRule: item.NameRule, VendorID: item.VendorID, UpdatedTime: item.UpdatedTime}
if vendor := byID[item.VendorID]; vendor != nil {
row.VendorName = vendor.Name
if operation.Action == "assign" && !seen[vendor.Id] {
preview.Sources = append(preview.Sources, *vendor)
seen[vendor.Id] = true
}
}
preview.Models = append(preview.Models, row)
counts[item.VendorID]++
}
if operation.Action == "delete" && len(models) > 0 {
return nil, &VendorReferenceError{Counts: counts}
}
slices.SortFunc(preview.Sources, func(a, b Vendor) int { return a.Id - b.Id })
encoded, err := common.Marshal([]any{preview, modelVersions})
if err != nil {
return nil, err
}
preview.Version = fmt.Sprintf("%x", sha256.Sum256(encoded))
return preview, nil
}
func PreviewVendorOperation(operation VendorOperation) (*VendorOperationPreview, error) {
return buildVendorOperationPreview(DB, operation)
}
func ApplyVendorOperation(operation VendorOperation) (*VendorOperationResult, error) {
if operation.ExpectedVersion == "" {
return nil, ErrVendorConflict
}
return applyVendorOperation(operation)
}
func DeleteVendors(ids []int) error {
_, err := applyVendorOperation(VendorOperation{Action: "delete", VendorIDs: ids})
return err
}
func applyVendorOperation(operation VendorOperation) (*VendorOperationResult, error) {
result := &VendorOperationResult{UpdatedModels: []int{}, DeletedVendors: []int{}}
err := metadataTransaction(func(tx *gorm.DB) error {
preview, err := buildVendorOperationPreview(lockForUpdate(tx), operation)
if err != nil {
return err
}
if operation.ExpectedVersion != "" && operation.ExpectedVersion != preview.Version {
return ErrVendorConflict
}
for _, item := range preview.Models {
if item.VendorID != operation.TargetVendorID {
result.UpdatedModels = append(result.UpdatedModels, item.ID)
}
}
if len(result.UpdatedModels) > 0 {
if err := tx.Model(&Model{}).Where("id IN ?", result.UpdatedModels).Updates(map[string]any{"vendor_id": operation.TargetVendorID, "updated_time": common.GetTimestamp()}).Error; err != nil {
return err
}
}
if operation.Action == "merge" || operation.Action == "delete" {
for _, vendor := range preview.Sources {
result.DeletedVendors = append(result.DeletedVendors, vendor.Id)
}
if err := tx.Where("id IN ?", result.DeletedVendors).Delete(&Vendor{}).Error; err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
RefreshPricing()
return result, nil
}