fix(*): 模型选择 - 修复系统渠道 Logo 搜索与前台模型模式误判

This commit is contained in:
ddCat
2026-08-27 23:43:54 +08:00
parent b3e977a985
commit 2c02f5ef3d
4 changed files with 117 additions and 26 deletions

View File

@@ -44,29 +44,9 @@ func (s *Service) CreateTask(userID string, req CreateTaskRequest) (*model.Task,
}
if !workflowProviderTask {
if frontendEnabled {
// 前台模型模式:必须有 logicalModelId
if logicalModelID == "" {
return nil, InvalidModelSelection("前台模型模式下必须指定 logicalModelId")
}
intent := ModelRequestIntentFromTaskInput(normalizedInput, taskType, req.Operation)
routed, err = s.ResolveLogicalModel(logicalModelID, intent)
if err != nil {
return nil, err
}
normalizedInput = applyRoutedProviderSelection(normalizedInput, routed)
} else {
// 系统渠道模型模式:禁止 logicalModelId
if logicalModelID != "" {
return nil, ModelCatalogMismatch("模型目录已更新,请重新选择")
}
// 自定义渠道没有系统 channelId;它会在后续由自定义渠道功能开关、
// 能力校验和 provider 配置校验共同处理,不能误报为“缺少系统渠道”。
if !taskInputUsesCustomChannel(normalizedInput) {
if err := s.validateSystemChannelModelSelection(normalizedInput); err != nil {
return nil, err
}
}
routed, normalizedInput, err = s.resolveTaskModelSelection(normalizedInput, logicalModelID, taskType, req.Operation, frontendEnabled)
if err != nil {
return nil, err
}
}
@@ -146,6 +126,34 @@ func (s *Service) CreateTask(userID string, req CreateTaskRequest) (*model.Task,
return taskForOutput(task), nil
}
// resolveTaskModelSelection 根据请求实际携带的模型选择决定路由方式。
// 显式系统渠道请求不能被全局前台模型开关误判,但仍必须通过渠道、模型和价格校验。
func (s *Service) resolveTaskModelSelection(input map[string]any, logicalModelID string, taskType string, operation string, frontendEnabled bool) (*RoutedModel, map[string]any, error) {
if frontendEnabled && !taskInputUsesSystemChannel(input) {
if logicalModelID == "" {
return nil, input, InvalidModelSelection("前台模型模式下必须指定 logicalModelId")
}
intent := ModelRequestIntentFromTaskInput(input, taskType, operation)
routed, err := s.ResolveLogicalModel(logicalModelID, intent)
if err != nil {
return nil, input, err
}
return routed, applyRoutedProviderSelection(input, routed), nil
}
if logicalModelID != "" {
return nil, input, ModelCatalogMismatch("模型目录已更新,请重新选择")
}
// 自定义渠道没有系统 channelId;它会在后续由自定义渠道功能开关、
// 能力校验和 provider 配置校验共同处理,不能误报为“缺少系统渠道”。
if !taskInputUsesCustomChannel(input) {
if err := s.validateSystemChannelModelSelection(input); err != nil {
return nil, input, err
}
}
return nil, input, nil
}
func applyRoutedProviderSelection(input map[string]any, routed *RoutedModel) map[string]any {
config, _ := input["config"].(map[string]any)
nextConfig := make(map[string]any, len(config)+2)
@@ -331,6 +339,15 @@ func taskInputUsesCustomChannel(input map[string]any) bool {
return strings.TrimSpace(baseURL) != "" && strings.TrimSpace(apiKey) != ""
}
func taskInputUsesSystemChannel(input map[string]any) bool {
config, ok := input["config"].(map[string]any)
if !ok {
return false
}
channelID, _ := config["channelId"].(string)
return strings.TrimSpace(channelID) != ""
}
func taskInputUsesWorkflowProvider(input map[string]any) bool {
config, ok := input["config"].(map[string]any)
if !ok {

View File

@@ -1,6 +1,15 @@
package service
import "testing"
import (
"strings"
"testing"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/repository"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func TestTaskInputUsesWorkflowProvider(t *testing.T) {
tests := []struct {
@@ -26,3 +35,49 @@ func TestTaskInputUsesWorkflowProvider(t *testing.T) {
})
}
}
func TestResolveTaskModelSelectionAllowsExplicitSystemChannelWhenFrontendModelsEnabled(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:"+newID()+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.ModelChannel{}, &model.ChannelModel{}, &model.ChannelModelPriceTier{}); err != nil {
t.Fatal(err)
}
channel := model.ModelChannel{ID: "channel-1", Scope: model.ChannelScopeSystem, Enabled: true, Name: "Agnes"}
channelModel := model.ChannelModel{
ID: "channel-model-1", ChannelID: channel.ID, ModelKey: "agnes-video-2.5", Capability: "video",
Protocol: model.ChannelInterfaceNewAPIVideo, BillingMode: "per_second", PriceConfigured: true, Enabled: true,
}
if err := db.Create(&channel).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&channelModel).Error; err != nil {
t.Fatal(err)
}
input, err := normalizeTaskInput(map[string]any{
"mode": "video",
"config": providerConfig{ChannelID: channel.ID, Model: channelModel.ModelKey, InterfaceType: string(channelModel.Protocol)},
})
if err != nil {
t.Fatal(err)
}
routed, resolvedInput, err := (&Service{repo: repository.New(db)}).resolveTaskModelSelection(input, "", "canvas_video", "", true)
if err != nil {
t.Fatalf("resolveTaskModelSelection() error = %v", err)
}
if routed != nil || resolvedInput["config"] == nil {
t.Fatalf("resolveTaskModelSelection() routed = %#v, input = %#v", routed, resolvedInput)
}
}
func TestResolveTaskModelSelectionStillRequiresLogicalModelWithoutExplicitSystemChannel(t *testing.T) {
_, _, err := (&Service{}).resolveTaskModelSelection(map[string]any{
"mode": "video",
"config": map[string]any{"model": "agnes-video-2.5"},
}, "", "canvas_video", "", true)
if err == nil || !strings.Contains(err.Error(), "logicalModelId") {
t.Fatalf("resolveTaskModelSelection() error = %v, want logicalModelId validation", err)
}
}