Files
ttstd-infinite-canvas/backend/internal/service/channel_model_catalog_plugin.go
dingxin150015-commits ea236b5956 feat(provider): add plugin system for service-specific model discovery
Model discovery currently assumes every provider exposes all models via
GET {baseURL}/models. Providers like Alibaba Cloud Bailian serve video,
3D and some image models through dedicated endpoints, so 21 models across
5 families (HappyHorse / Wan / Kling / PixVerse / Vidu / Tripo) are never
discoverable - a functional blocker for a video creation workbench.

This introduces a ModelDiscovery plugin interface with a registry that
matches vendor plugins by base URL and falls back to the standard OpenAI
implementation, keeping core discovery logic vendor-agnostic.

- add provider.ModelDiscovery interface and plugin registry
- add openai plugin (standard behaviour, default fallback)
- add bailian plugin (standard + 21 extended multimodal models)
- gate behind ENABLE_PROVIDER_PLUGINS, default false
- register plugins lazily from service layer to avoid import cycle

Verified locally: flag off -> 238 models (unchanged); flag on -> 257
models (238 + 19 net new after dedup). OpenAI and Gemini channels
unaffected in both modes.
2026-08-17 13:47:27 +08:00

139 lines
3.5 KiB
Go

package service
import (
"context"
"fmt"
"os"
"sort"
"strings"
"sync"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/provider"
"infinite-canvas/backend/internal/provider/bailian"
"infinite-canvas/backend/internal/provider/openai"
)
var (
pluginsInitialized bool
pluginsInitMutex sync.Mutex
)
// initPlugins 初始化并注册所有插件(只执行一次)
func initPlugins() {
pluginsInitMutex.Lock()
defer pluginsInitMutex.Unlock()
if pluginsInitialized {
return
}
// 注册 OpenAI 标准插件(默认)
provider.Register(openai.New())
// 注册阿里云百炼插件
provider.Register(bailian.New())
// 未来可以添加更多插件:
// provider.Register(gemini.New())
// provider.Register(anthropic.New())
pluginsInitialized = true
}
// FetchChannelModelCatalogWithPlugin 使用插件机制拉取模型(新实现)
func (s *Service) FetchChannelModelCatalogWithPlugin(ctx context.Context, actor *model.User, input ChannelModelsRequest) ([]ChannelModelCatalogItem, error) {
// 确保插件已初始化
initPlugins()
// 验证基本参数
if actor == nil || strings.TrimSpace(actor.ID) == "" {
return nil, Unauthorized("请先登录")
}
baseURL := strings.TrimRight(strings.TrimSpace(input.BaseURL), "/")
apiKey := strings.TrimSpace(input.APIKey)
if baseURL == "" {
return nil, BadAuthRequest("请填写 Base URL")
}
if apiKey == "" {
return nil, BadAuthRequest("请填写 API Key")
}
// 1. 匹配插件
headers := s.headersToMap(input.Headers)
discovery := provider.MatchProvider(baseURL, headers)
if discovery == nil {
return nil, fmt.Errorf("未找到匹配的服务商插件,请检查 Base URL")
}
// 2. 构建配置
config := provider.DiscoveryConfig{
BaseURL: baseURL,
APIKey: apiKey,
APIFormat: strings.ToLower(strings.TrimSpace(input.APIFormat)),
Headers: headers,
AllowLocalChannel: input.AllowLocalChannel,
Region: s.extractRegion(baseURL),
}
if config.APIFormat == "" {
config.APIFormat = "openai"
}
// 3. 调用插件发现模型
models, err := discovery.DiscoverModels(ctx, config)
if err != nil {
return nil, fmt.Errorf("插件发现模型失败: %w", err)
}
// 4. 转换为内部格式
catalog := make([]ChannelModelCatalogItem, 0, len(models))
for _, m := range models {
catalog = append(catalog, ChannelModelCatalogItem{
ID: m.ID,
SupportedEndpointTypes: m.SupportedEndpointTypes,
})
}
// 5. 排序
sort.Slice(catalog, func(i, j int) bool {
return catalog[i].ID < catalog[j].ID
})
return catalog, nil
}
// headersToMap 将 OutboundHeader 数组转换为 map
func (s *Service) headersToMap(headers []OutboundHeader) map[string]string {
result := make(map[string]string, len(headers))
for _, h := range headers {
if h.Name != "" && h.Value != "" {
result[h.Name] = h.Value
}
}
return result
}
// extractRegion 从 baseURL 提取地域信息
func (s *Service) extractRegion(baseURL string) string {
// 美国地域
if strings.Contains(baseURL, "dashscope-us") {
return "us-east-1"
}
// 亚太地域(新加坡)
if strings.Contains(baseURL, "ap-southeast-1") {
return "ap-southeast-1"
}
// 默认:中国北京
return "cn-beijing"
}
// isPluginEnabled 检查插件机制是否启用
func (s *Service) isPluginEnabled() bool {
// 通过环境变量控制
value := os.Getenv("ENABLE_PROVIDER_PLUGINS")
return strings.ToLower(value) == "true" || value == "1"
}