mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-16 07:33:06 +08:00
后端新增 BuildPlatformCatalog:平台清单加上多协议供应商 profile(接入模式、 各协议默认基址、内置分流规则),由测试 TestFrontendBuiltinPlatformCatalogInSync 写成前端内置清单 constants/platformCatalog.builtin.json 并校验一致。前端随后端 一同构建发布,直接使用该清单,取代 credentialsBuilder.ts 中需要手工同步的默认 端点与 OpenCode 分流规则。 改为读取平台清单: - 平台选项 CONCRETE_PLATFORM_OPTIONS / GROUP_PLATFORM_OPTIONS、平台展示名 platformLabel 以及各处 i18n 平台名的默认值; - 渠道定价平台与组合分组平台、用户平台额度、上游模型同步可用平台; - 多协议供应商判断、原生协议能力、各接入模式的默认端点与内置分流规则、 请求头覆写资格; - 系统默认额度与各登录方式默认额度:此前前端只列出 5 个平台(新增国产厂商 与 OpenCode 时漏改),后端一直支持全部平台,现在按清单列出全部平台。 没有专属界面的多协议供应商走通用表单:创建弹窗列出其按钮,接入模式、可选 协议、默认端点与分流规则都来自 profile(按模型分流的供应商显示分流规则编辑 器);编辑弹窗同样适用。未登记专属图标 / 配色的平台使用通用图标与中性配色。 平台按钮分三行:国际平台、国产厂商、多模型聚合平台(OpenCode 与平台清单中 登记的其他多协议供应商)。没有内置模型列表的供应商不预填模型白名单(此前 回落 Claude 模型列表,新账号只接受这些模型),API Key 输入框也不再显示 Claude 的占位符与说明。 AccountPlatform / GroupPlatform 类型放宽为"内置平台 | 任意字符串"。新登记 平台只需重新生成内置清单,无需修改前端代码。 另修正编辑弹窗两处遗漏 MiniMax 的默认端点回退(清空 Base URL 时曾回落到 Anthropic 默认地址)。 测试:credentialsBuilder 以改动前实现为基准,逐一比较全部平台 × 接入模式 × 协议的默认端点与各判断;替换清单后平台选项、展示名、额度列表随之更新;创建 / 编辑弹窗对走通用表单的供应商(按模型 / 按入站两种分流)提交的凭证。更新了 两个固定旧列表写法的测试。平台清单与账号表单的测试加入 CI 前端关键测试列表 (FRONTEND_CRITICAL_VITEST)。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
5.2 KiB
Go
141 lines
5.2 KiB
Go
//go:build unit
|
|
|
|
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/domain"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestBuildPlatformCatalogMirrorsPlatformListAndProfiles(t *testing.T) {
|
|
catalog := BuildPlatformCatalog()
|
|
require.Equal(t, domain.CompositePrecedencePlatformIDs(), catalog.CompositePrecedence)
|
|
require.Len(t, catalog.Platforms, len(domain.ConcretePlatformIDs()))
|
|
|
|
for i, entry := range catalog.Platforms {
|
|
require.Equal(t, domain.ConcretePlatformIDs()[i], entry.ID)
|
|
profile := LookupProviderProfile(entry.ID)
|
|
if profile == nil {
|
|
require.Nil(t, entry.MultiProtocol, entry.ID)
|
|
continue
|
|
}
|
|
require.NotNil(t, entry.MultiProtocol, entry.ID)
|
|
require.Equal(t, profile.DefaultMode, entry.MultiProtocol.DefaultMode, entry.ID)
|
|
require.Equal(t, profile.DefaultMode, entry.MultiProtocol.Modes[0].Mode, entry.ID)
|
|
require.Equal(t, profile.ResponsesPath, entry.MultiProtocol.ResponsesPath, entry.ID)
|
|
require.Len(t, entry.MultiProtocol.Modes, len(profile.Modes), entry.ID)
|
|
for _, mode := range entry.MultiProtocol.Modes {
|
|
endpoints := profile.Modes[mode.Mode]
|
|
require.Equal(t, endpoints.BaseURLs, mode.BaseURLs, "%s/%s", entry.ID, mode.Mode)
|
|
require.Equal(t, len(endpoints.ProtocolRules), len(mode.ProtocolRules), "%s/%s", entry.ID, mode.Mode)
|
|
for j, rule := range endpoints.ProtocolRules {
|
|
require.Equal(t, rule, mode.ProtocolRules[j])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildPlatformCatalogJSONShape(t *testing.T) {
|
|
raw, err := json.Marshal(BuildPlatformCatalog())
|
|
require.NoError(t, err)
|
|
|
|
type modeJSON struct {
|
|
Mode string `json:"mode"`
|
|
BaseURLs map[string]string `json:"base_urls"`
|
|
ProtocolRules []ProtocolRule `json:"protocol_rules"`
|
|
}
|
|
type platformJSON struct {
|
|
ID string `json:"id"`
|
|
DisplayName string `json:"display_name"`
|
|
Gateway string `json:"gateway"`
|
|
CNProvider *bool `json:"cn_provider"`
|
|
MultiProtocol *struct {
|
|
DefaultMode string `json:"default_mode"`
|
|
Routing string `json:"routing"`
|
|
ResponsesPath string `json:"responses_path"`
|
|
Modes []modeJSON `json:"modes"`
|
|
} `json:"multi_protocol"`
|
|
}
|
|
var decoded struct {
|
|
Platforms []platformJSON `json:"platforms"`
|
|
CompositePrecedence []string `json:"composite_precedence"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(raw, &decoded))
|
|
require.Equal(t, domain.CompositePrecedencePlatformIDs(), decoded.CompositePrecedence)
|
|
byID := map[string]platformJSON{}
|
|
for _, p := range decoded.Platforms {
|
|
byID[p.ID] = p
|
|
}
|
|
|
|
anthropic := byID[PlatformAnthropic]
|
|
require.Equal(t, "Anthropic", anthropic.DisplayName)
|
|
require.Equal(t, "anthropic", anthropic.Gateway)
|
|
require.NotNil(t, anthropic.CNProvider)
|
|
require.False(t, *anthropic.CNProvider)
|
|
require.Nil(t, anthropic.MultiProtocol)
|
|
require.NotContains(t, string(raw), `"multi_protocol":null`)
|
|
|
|
deepseek := byID[PlatformDeepseek].MultiProtocol
|
|
require.NotNil(t, deepseek)
|
|
require.Equal(t, "by_inbound", deepseek.Routing)
|
|
require.Equal(t, "/responses", deepseek.ResponsesPath)
|
|
|
|
openCode := byID[PlatformOpenCodeGo].MultiProtocol
|
|
require.NotNil(t, openCode)
|
|
require.Equal(t, "by_model", openCode.Routing)
|
|
require.Equal(t, AccountModeGo, openCode.DefaultMode)
|
|
require.Len(t, openCode.Modes, 2)
|
|
require.Equal(t, AccountModeGo, openCode.Modes[0].Mode)
|
|
require.Equal(t, DefaultOpenCodeGoBaseURL, openCode.Modes[0].BaseURLs[APIProtocolChatCompletions])
|
|
require.Equal(t, DefaultOpenCodeGoProtocolRules(), openCode.Modes[0].ProtocolRules)
|
|
require.Equal(t, AccountModeZen, openCode.Modes[1].Mode)
|
|
}
|
|
|
|
// 下发的是副本:调用方修改结果不影响内置 profile。
|
|
func TestBuildPlatformCatalogReturnsCopies(t *testing.T) {
|
|
catalog := BuildPlatformCatalog()
|
|
for _, entry := range catalog.Platforms {
|
|
if entry.MultiProtocol == nil {
|
|
continue
|
|
}
|
|
for _, mode := range entry.MultiProtocol.Modes {
|
|
for protocol := range mode.BaseURLs {
|
|
mode.BaseURLs[protocol] = "mutated"
|
|
}
|
|
for i := range mode.ProtocolRules {
|
|
mode.ProtocolRules[i].Protocol = "mutated"
|
|
}
|
|
}
|
|
}
|
|
require.Equal(t, DefaultOpenCodeGoBaseURL, LookupProviderProfile(PlatformOpenCodeGo).DefaultBaseURL(AccountModeGo, APIProtocolChatCompletions))
|
|
require.NotEqual(t, "mutated", LookupProviderProfile(PlatformOpenCodeGo).Modes[AccountModeGo].ProtocolRules[0].Protocol)
|
|
}
|
|
|
|
// 前端内置的平台清单(接口加载前与加载失败时使用)必须与后端一致。
|
|
// 更新平台清单或 profile 后运行:
|
|
//
|
|
// UPDATE_PLATFORM_CATALOG=1 go test -tags unit ./internal/service/ -run TestFrontendBuiltinPlatformCatalogInSync
|
|
func TestFrontendBuiltinPlatformCatalogInSync(t *testing.T) {
|
|
const path = "../../../frontend/src/constants/platformCatalog.builtin.json"
|
|
want, err := json.MarshalIndent(BuildPlatformCatalog(), "", " ")
|
|
require.NoError(t, err)
|
|
want = append(want, '\n')
|
|
|
|
if os.Getenv("UPDATE_PLATFORM_CATALOG") == "1" {
|
|
require.NoError(t, os.WriteFile(path, want, 0o644))
|
|
return
|
|
}
|
|
got, err := os.ReadFile(path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
t.Skip("frontend sources not available")
|
|
}
|
|
require.NoError(t, err)
|
|
require.JSONEq(t, string(want), string(got),
|
|
"frontend/src/constants/platformCatalog.builtin.json 与后端平台清单不一致,按测试注释重新生成")
|
|
}
|