Files
ttstd-infinite-canvas/backend/internal/generation/registry.go
T

246 lines
7.9 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.ChannelInterfaceDoubaoStreamingTTS):
return "豆包流式语音", 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()
}