独立 /api/plan/v3 Seedream、Seedance 插件,并同步宿主能力默认值与渠道识别,避免与官方 /api/v3 凭证混用。 Co-authored-by: Cursor <cursoragent@cursor.com>
244 lines
7.8 KiB
Go
244 lines
7.8 KiB
Go
package generation
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
|
||
"infinite-canvas/backend/internal/model"
|
||
"infinite-canvas/backend/internal/protocol"
|
||
)
|
||
|
||
type protocolRegistryContextKey struct{}
|
||
|
||
var emptyProtocolRegistry, _ = protocol.NewRegistry()
|
||
var officialFallbackRegistryMu sync.Mutex
|
||
var officialFallbackRegistryLoaded bool
|
||
var officialFallbackRegistry = emptyProtocolRegistry
|
||
|
||
func WithProtocolRegistry(ctx context.Context, registry *protocol.Registry) context.Context {
|
||
return context.WithValue(ctx, protocolRegistryContextKey{}, registry)
|
||
}
|
||
|
||
func ProtocolRegistryFromContext(ctx context.Context) (*protocol.Registry, bool) {
|
||
registry, ok := ctx.Value(protocolRegistryContextKey{}).(*protocol.Registry)
|
||
return registry, ok && registry != nil
|
||
}
|
||
|
||
func ProtocolAdapterForContext(ctx context.Context, id string) (protocol.Adapter, bool) {
|
||
registry, _ := ctx.Value(protocolRegistryContextKey{}).(*protocol.Registry)
|
||
if registry == nil {
|
||
registry = emptyProtocolRegistry
|
||
}
|
||
return registry.Resolve(strings.TrimSpace(id))
|
||
}
|
||
|
||
// EnsureOfficialProtocolAdapter 让未注入 registry 的调用(主要是单测)与生产一样
|
||
// 使用官方插件包。调用方若显式放入空 registry,表示要测“插件未安装”。
|
||
func EnsureOfficialProtocolAdapter(ctx context.Context, interfaceType string) context.Context {
|
||
if _, present := ProtocolRegistryFromContext(ctx); present {
|
||
return ctx
|
||
}
|
||
interfaceType = strings.TrimSpace(interfaceType)
|
||
if interfaceType == "" {
|
||
return ctx
|
||
}
|
||
registry := LoadOfficialFallbackRegistry()
|
||
adapter, ok := registry.Resolve(interfaceType)
|
||
if !ok || adapter.Metadata().Execution != "declarative" {
|
||
return ctx
|
||
}
|
||
return WithProtocolRegistry(ctx, registry)
|
||
}
|
||
|
||
func OfficialDeclarativeVideoInterface(interfaceType string) (string, bool) {
|
||
switch strings.TrimSpace(interfaceType) {
|
||
case string(model.ChannelInterfaceAgnesVideo):
|
||
return "Agnes", true
|
||
case string(model.ChannelInterfaceMiniMaxVideo):
|
||
return "MiniMax", true
|
||
case string(model.ChannelInterfaceGeminiVeo):
|
||
return "Gemini Veo", true
|
||
case string(model.ChannelInterfaceNovitaVideo):
|
||
return "Novita", true
|
||
case string(model.ChannelInterfaceNewAPIChannel2):
|
||
return "NewAPI Video Generations", true
|
||
case string(model.ChannelInterfaceNewAPIChannel1):
|
||
return "NewAPI 媒体任务", true
|
||
case string(model.ChannelInterfaceXAIVideo):
|
||
return "xAI", true
|
||
case string(model.ChannelInterfaceVolcengineArkVideo), string(model.ChannelInterfaceVolcengineArkAgentPlanVideo):
|
||
return "火山方舟", true
|
||
case string(model.ChannelInterfaceVolcengineJiMengVideo):
|
||
return "即梦", true
|
||
case string(model.ChannelInterfaceNewAPIVideo):
|
||
return "OpenAI Videos", true
|
||
default:
|
||
return "", false
|
||
}
|
||
}
|
||
|
||
func OfficialDeclarativeImageInterface(interfaceType string) (string, bool) {
|
||
switch strings.TrimSpace(interfaceType) {
|
||
case string(model.ChannelInterfaceGeminiImage):
|
||
return "Gemini Images", true
|
||
case string(model.ChannelInterfaceOpenAIImage):
|
||
return "OpenAI Images", true
|
||
case string(model.ChannelInterfaceGrokImage):
|
||
return "Grok Images", true
|
||
case string(model.ChannelInterfaceVolcengineArkImage):
|
||
return "火山方舟图片", true
|
||
case string(model.ChannelInterfaceVolcengineArkAgentPlanImage):
|
||
return "火山方舟 Agent Plan 图片", true
|
||
case string(model.ChannelInterfaceVolcengineJiMengImage):
|
||
return "即梦图片", true
|
||
default:
|
||
return "", false
|
||
}
|
||
}
|
||
|
||
func OfficialDeclarativeAudioInterface(interfaceType string) (string, bool) {
|
||
switch strings.TrimSpace(interfaceType) {
|
||
case string(model.ChannelInterfaceOpenAIAudio):
|
||
return "OpenAI Audio", true
|
||
case string(model.ChannelInterfaceAsyncAudio):
|
||
return "异步音频", true
|
||
default:
|
||
return "", false
|
||
}
|
||
}
|
||
|
||
func DeclarativeProtocolAdapterForContext(ctx context.Context, id string) (protocol.Adapter, bool) {
|
||
adapter, ok := ProtocolAdapterForContext(ctx, id)
|
||
if !ok || adapter.Metadata().Execution != "declarative" {
|
||
return nil, false
|
||
}
|
||
return adapter, true
|
||
}
|
||
|
||
func AgentProtocolAdapterForContext(ctx context.Context, id string) (protocol.AgentAdapter, bool) {
|
||
registry, _ := ctx.Value(protocolRegistryContextKey{}).(*protocol.Registry)
|
||
if registry == nil {
|
||
registry = emptyProtocolRegistry
|
||
}
|
||
adapter, ok := registry.Resolve(strings.TrimSpace(id))
|
||
if !ok || adapter.Metadata().Execution != "declarative" {
|
||
return nil, false
|
||
}
|
||
agentAdapter, ok := adapter.(protocol.AgentAdapter)
|
||
if !ok {
|
||
return nil, false
|
||
}
|
||
capability, ok := adapter.(protocol.AgentCapability)
|
||
return agentAdapter, ok && capability.AgentAvailable()
|
||
}
|
||
|
||
func LoadOfficialFallbackRegistry() *protocol.Registry {
|
||
officialFallbackRegistryMu.Lock()
|
||
defer officialFallbackRegistryMu.Unlock()
|
||
if officialFallbackRegistryLoaded {
|
||
return officialFallbackRegistry
|
||
}
|
||
load := func() {
|
||
directory, err := OfficialPluginPackageDir()
|
||
if err != nil {
|
||
return
|
||
}
|
||
entries, err := os.ReadDir(directory)
|
||
if err != nil {
|
||
return
|
||
}
|
||
adapters := make([]protocol.Adapter, 0, len(entries))
|
||
for _, entry := range entries {
|
||
if entry.IsDir() || !strings.HasSuffix(strings.ToLower(entry.Name()), ".yingce-plugin") {
|
||
continue
|
||
}
|
||
data, err := os.ReadFile(filepath.Join(directory, entry.Name()))
|
||
if err != nil {
|
||
return
|
||
}
|
||
pkg, err := protocol.ParsePluginPackage(data)
|
||
if err != nil {
|
||
return
|
||
}
|
||
providers, err := protocol.LoadInstalledProviders(pkg.ManifestRaw, nil)
|
||
if err != nil {
|
||
return
|
||
}
|
||
adapters = append(adapters, providers...)
|
||
}
|
||
registry, err := protocol.NewRegistry(adapters...)
|
||
if err == nil {
|
||
officialFallbackRegistry = registry
|
||
officialFallbackRegistryLoaded = true
|
||
}
|
||
}
|
||
load()
|
||
return officialFallbackRegistry
|
||
}
|
||
|
||
func OfficialPluginPackageDir() (string, error) {
|
||
if configured := strings.TrimSpace(os.Getenv("CANVAS_OFFICIAL_PLUGIN_DIR")); configured != "" {
|
||
info, err := os.Stat(configured)
|
||
if err != nil || !info.IsDir() {
|
||
return "", fmt.Errorf("CANVAS_OFFICIAL_PLUGIN_DIR 不是可读目录:%s", configured)
|
||
}
|
||
return configured, nil
|
||
}
|
||
workingDir, err := os.Getwd()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
candidates := []string{"/app/plugin-packages"}
|
||
current := workingDir
|
||
for range 8 {
|
||
candidates = append(candidates, filepath.Join(current, "plugin-packages"))
|
||
parent := filepath.Dir(current)
|
||
if parent == current {
|
||
break
|
||
}
|
||
current = parent
|
||
}
|
||
for _, candidate := range candidates {
|
||
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
|
||
return candidate, nil
|
||
}
|
||
}
|
||
return "", fmt.Errorf("未找到官方插件目录 plugin-packages")
|
||
}
|
||
|
||
// 兼容包内未导出旧名(service 薄包装与未改调用点)。
|
||
func withProtocolRegistry(ctx context.Context, registry *protocol.Registry) context.Context {
|
||
return WithProtocolRegistry(ctx, registry)
|
||
}
|
||
func protocolRegistryFromContext(ctx context.Context) (*protocol.Registry, bool) {
|
||
return ProtocolRegistryFromContext(ctx)
|
||
}
|
||
func protocolAdapterForContext(ctx context.Context, id string) (protocol.Adapter, bool) {
|
||
return ProtocolAdapterForContext(ctx, id)
|
||
}
|
||
func ensureOfficialProtocolAdapter(ctx context.Context, interfaceType string) context.Context {
|
||
return EnsureOfficialProtocolAdapter(ctx, interfaceType)
|
||
}
|
||
func officialDeclarativeVideoInterface(interfaceType string) (string, bool) {
|
||
return OfficialDeclarativeVideoInterface(interfaceType)
|
||
}
|
||
func officialDeclarativeImageInterface(interfaceType string) (string, bool) {
|
||
return OfficialDeclarativeImageInterface(interfaceType)
|
||
}
|
||
func officialDeclarativeAudioInterface(interfaceType string) (string, bool) {
|
||
return OfficialDeclarativeAudioInterface(interfaceType)
|
||
}
|
||
func declarativeProtocolAdapterForContext(ctx context.Context, id string) (protocol.Adapter, bool) {
|
||
return DeclarativeProtocolAdapterForContext(ctx, id)
|
||
}
|
||
func agentProtocolAdapterForContext(ctx context.Context, id string) (protocol.AgentAdapter, bool) {
|
||
return AgentProtocolAdapterForContext(ctx, id)
|
||
}
|
||
func loadOfficialFallbackRegistry() *protocol.Registry {
|
||
return LoadOfficialFallbackRegistry()
|
||
}
|