fix(security): 模型目录 - 收敛插件出站边界并保留补充能力元数据
This commit is contained in:
1 parent
115eb76bad
commit
0002cdd2cb
6 files changed
+193
-558
No files matched your search
@@ -104,12 +104,6 @@ type ChannelModelCatalogOption struct {
|
||||
}
|
||||
|
||||
func (s *Service) FetchChannelModelCatalog(ctx context.Context, actor *model.User, input ChannelModelsRequest) ([]ChannelModelCatalogItem, error) {
|
||||
// 功能开关:如果启用插件机制,使用新实现
|
||||
if s.isPluginEnabled() {
|
||||
return s.FetchChannelModelCatalogWithPlugin(ctx, actor, input)
|
||||
}
|
||||
|
||||
// 否则使用原有实现(向后兼容)
|
||||
if actor == nil || strings.TrimSpace(actor.ID) == "" {
|
||||
return nil, Unauthorized("请先登录")
|
||||
}
|
||||
@@ -206,6 +200,9 @@ func (s *Service) FetchChannelModelCatalog(ctx context.Context, actor *model.Use
|
||||
sort.Slice(catalog, func(left int, right int) bool {
|
||||
return catalog[left].ID < catalog[right].ID
|
||||
})
|
||||
if s.isPluginEnabled() {
|
||||
catalog = extendChannelModelCatalog(baseURL, apiFormat, headers, catalog)
|
||||
}
|
||||
return catalog, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,138 +1,117 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"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
|
||||
)
|
||||
var pluginsInitOnce sync.Once
|
||||
|
||||
// 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
|
||||
func initModelCatalogPlugins() {
|
||||
pluginsInitOnce.Do(func() {
|
||||
provider.Register(bailian.New())
|
||||
})
|
||||
|
||||
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
|
||||
// extendChannelModelCatalog 在已通过统一安全出站链路获取的标准目录上补充厂商条目。
|
||||
// 插件不持有密钥也不自行发 HTTP,避免绕过 SSRF、请求头和响应大小边界。
|
||||
func extendChannelModelCatalog(baseURL string, apiFormat string, headers []OutboundHeader, catalog []ChannelModelCatalogItem) []ChannelModelCatalogItem {
|
||||
initModelCatalogPlugins()
|
||||
discovery := provider.MatchProvider(baseURL, outboundHeadersMap(headers))
|
||||
if discovery == nil {
|
||||
return catalog
|
||||
}
|
||||
extra := discovery.AdditionalModels(provider.DiscoveryConfig{
|
||||
BaseURL: baseURL,
|
||||
APIFormat: apiFormat,
|
||||
Headers: outboundHeadersMap(headers),
|
||||
Region: modelCatalogRegion(baseURL),
|
||||
})
|
||||
if len(extra) == 0 {
|
||||
return catalog
|
||||
}
|
||||
|
||||
indexByID := make(map[string]int, len(catalog)+len(extra))
|
||||
for index := range catalog {
|
||||
indexByID[catalog[index].ID] = index
|
||||
}
|
||||
for _, model := range extra {
|
||||
item := providerCatalogItem(model)
|
||||
if item.ID == "" {
|
||||
continue
|
||||
}
|
||||
if index, exists := indexByID[item.ID]; exists {
|
||||
catalog[index] = enrichCatalogItem(catalog[index], item)
|
||||
continue
|
||||
}
|
||||
indexByID[item.ID] = len(catalog)
|
||||
catalog = append(catalog, item)
|
||||
}
|
||||
sort.Slice(catalog, func(left int, right int) bool {
|
||||
return catalog[left].ID < catalog[right].ID
|
||||
})
|
||||
return catalog
|
||||
}
|
||||
|
||||
func providerCatalogItem(item provider.Model) ChannelModelCatalogItem {
|
||||
modelType := ""
|
||||
for _, capability := range item.Capability {
|
||||
if normalized := normalizeCatalogModelType(capability); normalized != "" {
|
||||
modelType = normalized
|
||||
break
|
||||
}
|
||||
}
|
||||
return ChannelModelCatalogItem{
|
||||
ID: strings.TrimPrefix(strings.TrimSpace(item.ID), "models/"),
|
||||
DisplayName: strings.TrimSpace(item.DisplayName),
|
||||
ModelType: modelType,
|
||||
SupportedEndpointTypes: normalizeCatalogEndpointTypes(item.SupportedEndpointTypes),
|
||||
}
|
||||
}
|
||||
|
||||
func enrichCatalogItem(current ChannelModelCatalogItem, fallback ChannelModelCatalogItem) ChannelModelCatalogItem {
|
||||
if current.DisplayName == "" {
|
||||
current.DisplayName = fallback.DisplayName
|
||||
}
|
||||
if current.ModelType == "" {
|
||||
current.ModelType = fallback.ModelType
|
||||
}
|
||||
if len(current.SupportedEndpointTypes) == 0 {
|
||||
current.SupportedEndpointTypes = fallback.SupportedEndpointTypes
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
func outboundHeadersMap(headers []OutboundHeader) map[string]string {
|
||||
result := make(map[string]string, len(headers))
|
||||
for _, header := range headers {
|
||||
result[header.Name] = header.Value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// extractRegion 从 baseURL 提取地域信息
|
||||
func (s *Service) extractRegion(baseURL string) string {
|
||||
// 美国地域
|
||||
if strings.Contains(baseURL, "dashscope-us") {
|
||||
func modelCatalogRegion(baseURL string) string {
|
||||
parsed, err := url.Parse(strings.TrimSpace(baseURL))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
host := strings.ToLower(parsed.Hostname())
|
||||
if strings.Contains(host, "dashscope-us") {
|
||||
return "us-east-1"
|
||||
}
|
||||
|
||||
// 亚太地域(新加坡)
|
||||
if strings.Contains(baseURL, "ap-southeast-1") {
|
||||
if strings.Contains(host, "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"
|
||||
value := strings.ToLower(strings.TrimSpace(os.Getenv("ENABLE_PROVIDER_PLUGINS")))
|
||||
return value == "true" || value == "1"
|
||||
}
|
||||
Reference in new issue
Block a user