fix(*): 模型选择 - 修复系统渠道 Logo 搜索与前台模型模式误判
This commit is contained in:
1 parent
b3e977a985
commit
2c02f5ef3d
4 files changed
+117
-26
No files matched your search
@@ -44,29 +44,9 @@ func (s *Service) CreateTask(userID string, req CreateTaskRequest) (*model.Task,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !workflowProviderTask {
|
if !workflowProviderTask {
|
||||||
if frontendEnabled {
|
routed, normalizedInput, err = s.resolveTaskModelSelection(normalizedInput, logicalModelID, taskType, req.Operation, frontendEnabled)
|
||||||
// 前台模型模式:必须有 logicalModelId
|
if err != nil {
|
||||||
if logicalModelID == "" {
|
return nil, err
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +126,34 @@ func (s *Service) CreateTask(userID string, req CreateTaskRequest) (*model.Task,
|
|||||||
return taskForOutput(task), nil
|
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 {
|
func applyRoutedProviderSelection(input map[string]any, routed *RoutedModel) map[string]any {
|
||||||
config, _ := input["config"].(map[string]any)
|
config, _ := input["config"].(map[string]any)
|
||||||
nextConfig := make(map[string]any, len(config)+2)
|
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) != ""
|
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 {
|
func taskInputUsesWorkflowProvider(input map[string]any) bool {
|
||||||
config, ok := input["config"].(map[string]any)
|
config, ok := input["config"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
package service
|
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) {
|
func TestTaskInputUsesWorkflowProvider(t *testing.T) {
|
||||||
tests := []struct {
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ type LobeIconComponent = ComponentType<SVGProps<SVGSVGElement> & { size?: number
|
|||||||
|
|
||||||
// 只允许按需加载 Mono 组件。这里不能使用 eager glob:模型 Logo 目录有数百个 provider 模块,
|
// 只允许按需加载 Mono 组件。这里不能使用 eager glob:模型 Logo 目录有数百个 provider 模块,
|
||||||
// eager 会让每次进入工作区都发起数百个开发模块请求,即使用户从未打开 Logo 选择器。
|
// eager 会让每次进入工作区都发起数百个开发模块请求,即使用户从未打开 Logo 选择器。
|
||||||
const iconModules = import.meta.glob?.("../../node_modules/@lobehub/icons/es/*/components/Mono.js", { import: "default" }) || {};
|
const iconModules = import.meta.glob("../../node_modules/@lobehub/icons/es/*/components/Mono.js", { import: "default" });
|
||||||
const iconLoaders = Object.fromEntries(
|
const iconLoaders = Object.fromEntries(
|
||||||
Object.entries(iconModules)
|
Object.entries(iconModules)
|
||||||
.map(([path, loader]) => [path.match(/\/([^/]+)\/components\/Mono\.js$/)?.[1], loader])
|
.map(([path, loader]) => [path.match(/\/([^/]+)\/components\/Mono\.js$/)?.[1], loader])
|
||||||
@@ -19,7 +19,7 @@ const iconLoaders = Object.fromEntries(
|
|||||||
const iconRegistry = new Map<string, LobeIconComponent>();
|
const iconRegistry = new Map<string, LobeIconComponent>();
|
||||||
const iconLoadPromises = new Map<string, Promise<LobeIconComponent | undefined>>();
|
const iconLoadPromises = new Map<string, Promise<LobeIconComponent | undefined>>();
|
||||||
const iconOptions = toc
|
const iconOptions = toc
|
||||||
.filter((item) => item.group === "model" || item.group === "provider")
|
.filter((item) => item.group === "model" || item.group === "provider" || item.group === "application")
|
||||||
.map((item) => ({ id: item.id, title: item.fullTitle || item.title }))
|
.map((item) => ({ id: item.id, title: item.fullTitle || item.title }))
|
||||||
.filter((item) => Boolean(iconLoaders[item.id]));
|
.filter((item) => Boolean(iconLoaders[item.id]));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
describe("模型 Logo 选择器", () => {
|
||||||
|
test("使用 Vite 可识别的直接 glob 并包含前台模型 Logo 数据源", () => {
|
||||||
|
const source = readFileSync(resolve(import.meta.dir, "../src/components/model-logo.tsx"), "utf8");
|
||||||
|
const toc = JSON.parse(readFileSync(resolve(import.meta.dir, "../node_modules/@lobehub/icons/es/toc.json"), "utf8")) as Array<{ group: string; id: string }>;
|
||||||
|
const options = toc.filter((item) => item.group === "model" || item.group === "provider" || item.group === "application");
|
||||||
|
|
||||||
|
expect(source).toContain('import.meta.glob("../../node_modules/@lobehub/icons/es/*/components/Mono.js"');
|
||||||
|
expect(source).not.toContain("import.meta.glob?.(");
|
||||||
|
expect(options.length).toBeGreaterThan(300);
|
||||||
|
expect(options.some((item) => item.id === "OpenAI")).toBe(true);
|
||||||
|
expect(options.some((item) => item.id === "AgnesAI")).toBe(true);
|
||||||
|
expect(existsSync(resolve(import.meta.dir, "../node_modules/@lobehub/icons/es/OpenAI/components/Mono.js"))).toBe(true);
|
||||||
|
expect(existsSync(resolve(import.meta.dir, "../node_modules/@lobehub/icons/es/AgnesAI/components/Mono.js"))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user