Files
ttstd-infinite-canvas/backend/internal/service/protocol_registry.go

476 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/protocol"
)
type protocolRegistryContextKey struct{}
var emptyProtocolRegistry, _ = protocol.NewRegistry()
var officialFallbackRegistryOnce sync.Once
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)
}
// officialDeclarativeVideoInterface 列出已有官方声明式视频插件的 InterfaceType。
// 这些接口缺失 adapter 时必须报错,不能再走第二套手写实现。
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):
return "火山方舟", true
case string(model.ChannelInterfaceVolcengineJiMengVideo):
return "即梦", true
case string(model.ChannelInterfaceNewAPIVideo):
return "OpenAI Videos", 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()
}
type PluginProviderCatalogItem struct {
ID string `json:"id"`
Version string `json:"version"`
Name string `json:"name"`
Vendor string `json:"vendor"`
Categories []protocol.Capability `json:"categories"`
Scopes []protocol.Surface `json:"scopes"`
Create string `json:"create,omitempty"`
Poll string `json:"poll,omitempty"`
ContentType string `json:"contentType,omitempty"`
BaseURL string `json:"baseUrl,omitempty"`
Enabled bool `json:"enabled"`
UnavailableReason string `json:"unavailableReason,omitempty"`
Workflows []protocol.ManifestWorkflow `json:"workflows,omitempty"`
}
// PluginProviderCatalog projects provider and workflow contributions from the
// unified plugin registry for channel and creation settings.
func (s *Service) PluginProviderCatalog(scope, capability string, includeUnavailable bool) []PluginProviderCatalogItem {
wantScope := protocol.Surface(strings.TrimSpace(scope))
wantCapability := protocol.Capability(strings.TrimSpace(capability))
items := make([]PluginProviderCatalogItem, 0)
for _, plugin := range s.Plugins() {
for _, provider := range plugin.Manifest.Contributes.Providers {
if !containsPluginSurface(provider.Scopes, wantScope) || (wantCapability != "" && !containsPluginCapability(provider.Capabilities, wantCapability)) {
continue
}
item := PluginProviderCatalogItem{ID: provider.ID, Version: plugin.Manifest.Version, Name: provider.Label, Vendor: plugin.Manifest.Author, Categories: provider.Capabilities, Scopes: provider.Scopes, BaseURL: provider.BaseURL, Enabled: plugin.Status == "enabled", UnavailableReason: plugin.Error, Workflows: workflowsForProvider(plugin.Manifest.Contributes.Workflows, provider.ID)}
item.Create, item.Poll, item.ContentType = operationSummary(provider.Create), operationSummaryPtr(provider.Poll), provider.Create.ContentType
// The registry metadata is the canonical provider projection. This keeps
// host-backed dispatch paths out of every user-facing catalog consumer.
if adapter, ok := canonicalProviderAdapter(s.protocolRegistry(), provider.ID); ok {
metadata := adapter.Metadata()
item.Create, item.Poll, item.ContentType = metadata.Create, metadata.Poll, metadata.ContentType
}
if includeUnavailable || item.Enabled {
items = append(items, item)
}
}
}
return items
}
func canonicalProviderAdapter(registry *protocol.Registry, id string) (protocol.Adapter, bool) {
return registry.Resolve(id)
}
func containsPluginSurface(items []protocol.Surface, want protocol.Surface) bool {
for _, item := range items {
if item == want {
return true
}
}
return false
}
func containsPluginCapability(items []protocol.Capability, want protocol.Capability) bool {
for _, item := range items {
if item == want {
return true
}
}
return false
}
func workflowsForProvider(items []protocol.ManifestWorkflow, providerID string) []protocol.ManifestWorkflow {
result := make([]protocol.ManifestWorkflow, 0)
for _, item := range items {
if item.ProviderID == providerID {
result = append(result, item)
}
}
return result
}
func operationSummary(operation protocol.ManifestOperation) string {
path := strings.ReplaceAll(operation.Path, "{{model}}", "{model}")
path = strings.ReplaceAll(path, "{{taskId}}", "{task_id}")
return strings.ToUpper(operation.Method) + " " + path
}
func operationSummaryPtr(operation *protocol.ManifestOperation) string {
if operation == nil {
return ""
}
return operationSummary(*operation)
}
func (s *Service) protocolRegistry() *protocol.Registry {
if s.pluginRuntime != nil {
if registry := s.pluginRuntime.registrySnapshot(); registry != nil {
return registry
}
}
return loadOfficialFallbackRegistry()
}
func loadOfficialFallbackRegistry() *protocol.Registry {
officialFallbackRegistryOnce.Do(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
}
})
return officialFallbackRegistry
}
func (s *Service) protocolMetadata(id string) (protocol.Metadata, bool) {
adapter, ok := s.protocolRegistry().Resolve(strings.TrimSpace(id))
if !ok {
return protocol.Metadata{}, false
}
return adapter.Metadata(), true
}
func (s *Service) channelProtocolMetadata(id string) (protocol.Metadata, bool) {
return s.protocolMetadata(id)
}
func (s *Service) canonicalProtocolID(id string) (string, bool) {
adapter, ok := s.protocolRegistry().Resolve(strings.TrimSpace(id))
if !ok {
return "", false
}
return adapter.Metadata().ID, true
}
func (s *Service) protocolIsSelectable(id string) bool {
metadata, ok := s.channelProtocolMetadata(id)
return ok && metadata.Enabled && metadata.UnavailableReason == ""
}
func (s *Service) Plugins() []PluginView {
if s.pluginRuntime == nil {
return []PluginView{}
}
items := s.pluginRuntime.list()
for index := range items {
items[index].Management = pluginManagement(items[index].Manifest.ID, items[index].Source)
}
return items
}
// PluginsForUser keeps the plugin center response aligned with the public
// feature switch. Administrators must still be able to inspect and recover
// bundled plugins even when ordinary users cannot see them.
func (s *Service) PluginsForUser(actor *model.User) ([]PluginView, error) {
items := s.Plugins()
if actor != nil && actor.Role == model.UserRoleAdmin {
return items, nil
}
visible, err := s.FeatureEnabled(FeatureSystemPlugins)
if err != nil {
return nil, err
}
if visible {
return items, nil
}
filtered := make([]PluginView, 0, len(items))
for _, item := range items {
if item.Management.ActivationScope == PluginScopeUser {
filtered = append(filtered, item)
}
}
return filtered, nil
}
func (s *Service) InstallPlugin(data []byte, fileName string) (PluginView, error) {
if s.pluginRuntime == nil {
return PluginView{}, fmt.Errorf("插件运行时未初始化")
}
parsed, err := protocol.ParsePluginPackage(data)
if err != nil {
return PluginView{}, err
}
if err := s.ensurePaymentPluginLifecycle(parsed.Manifest); err != nil {
return PluginView{}, err
}
plugin, err := s.pluginRuntime.install(data, fileName)
if err == nil {
s.refreshPaymentRegistry()
}
return plugin, err
}
func (s *Service) InstallPluginForAdmin(actor *model.User, data []byte, fileName string) (PluginView, error) {
if err := s.RequireAdmin(actor); err != nil {
return PluginView{}, err
}
parsed, err := protocol.ParsePluginPackage(data)
if err != nil {
return PluginView{}, err
}
if _, reserved := officialApplicationPolicies[parsed.Manifest.Metadata.ID]; reserved {
return PluginView{}, fmt.Errorf("插件 ID %q 由官方应用保留", parsed.Manifest.Metadata.ID)
}
if _, reserved := systemPaymentPolicies[parsed.Manifest.Metadata.ID]; reserved && !isPaymentPluginManifest(parsed.Manifest) {
return PluginView{}, fmt.Errorf("插件 ID %q 由系统支付插件保留", parsed.Manifest.Metadata.ID)
}
plugin, err := s.InstallPlugin(data, fileName)
if err != nil {
return PluginView{}, err
}
plugin.Management = pluginManagement(plugin.Manifest.ID, plugin.Source)
now := time.Now()
state := &model.PluginPlatformState{PluginID: plugin.Manifest.ID, Available: plugin.Status == "enabled", UpdatedBy: actor.ID, CreatedAt: now, UpdatedAt: now}
if err := s.repo.SavePluginPlatformState(state); err != nil {
return PluginView{}, fmt.Errorf("保存插件平台状态:%w", err)
}
if err := s.appendAdminAudit(actor, "plugin.install", "plugin", plugin.Manifest.ID, "安装自定义插件", map[string]any{"fileName": fileName, "sha256": plugin.SHA256}); err != nil {
return PluginView{}, err
}
return plugin, nil
}
func (s *Service) PluginPackage(id string) ([]byte, string, error) {
if s.pluginRuntime == nil {
return nil, "", fmt.Errorf("插件运行时未初始化")
}
s.pluginRuntime.mu.RLock()
record, ok := s.pluginRuntime.plugins[strings.TrimSpace(id)]
s.pluginRuntime.mu.RUnlock()
if !ok {
return nil, "", fmt.Errorf("插件 %q 不存在", id)
}
if record.PackagePath == "" {
return nil, "", fmt.Errorf("插件 %q 没有可下载的包文件", id)
}
data, err := os.ReadFile(filepath.Join(s.pluginRuntime.packageDir, filepath.Base(record.PackagePath)))
if err != nil {
return nil, "", fmt.Errorf("读取插件包失败:%w", err)
}
return data, record.FileName, nil
}
func (s *Service) SetPluginEnabled(id string, enabled bool) (PluginView, error) {
if s.pluginRuntime == nil {
return PluginView{}, fmt.Errorf("插件运行时未初始化")
}
plugin, err := s.pluginRuntime.setEnabled(id, enabled)
if err == nil {
s.refreshPaymentRegistry()
}
return plugin, err
}
func (s *Service) UninstallPlugin(id string) error {
if s.pluginRuntime == nil {
return fmt.Errorf("插件运行时未初始化")
}
if err := s.ensurePaymentPluginCanBeRemoved(id); err != nil {
return err
}
err := s.pluginRuntime.uninstall(id)
if err == nil {
s.refreshPaymentRegistry()
}
return err
}
func (s *Service) ensurePaymentPluginLifecycle(next protocol.Manifest) error {
if s.repo == nil || s.pluginRuntime == nil || len(next.Contributes.PaymentProviders) == 0 {
return nil
}
s.pluginRuntime.mu.RLock()
current, exists := s.pluginRuntime.plugins[next.Metadata.ID]
s.pluginRuntime.mu.RUnlock()
if !exists {
return nil
}
var currentManifest protocol.Manifest
if err := json.Unmarshal(current.Raw, &currentManifest); err != nil {
return fmt.Errorf("读取现有插件 %q:%w", next.Metadata.ID, err)
}
if len(currentManifest.Contributes.PaymentProviders) == 0 || strings.TrimSpace(currentManifest.Metadata.Version) == strings.TrimSpace(next.Metadata.Version) {
return nil
}
count, err := s.repo.ActivePaymentOrderCountForPlugin(currentManifest.Metadata.ID, currentManifest.Metadata.Version)
if err != nil {
return fmt.Errorf("检查插件 %q 未完成订单:%w", current.Metadata.ID, err)
}
if count > 0 {
return fmt.Errorf("支付插件 %q 仍有 %d 个未完成订单,暂不能升级", current.Metadata.ID, count)
}
return nil
}
func (s *Service) ensurePaymentPluginCanBeRemoved(id string) error {
if s.repo == nil || s.pluginRuntime == nil {
return nil
}
s.pluginRuntime.mu.RLock()
record, exists := s.pluginRuntime.plugins[strings.TrimSpace(id)]
s.pluginRuntime.mu.RUnlock()
if !exists {
return nil
}
var manifest protocol.Manifest
if err := json.Unmarshal(record.Raw, &manifest); err != nil {
return fmt.Errorf("读取插件 %q:%w", id, err)
}
if len(manifest.Contributes.PaymentProviders) == 0 {
return nil
}
count, err := s.repo.ActivePaymentOrderCountForPlugin(manifest.Metadata.ID, "")
if err != nil {
return fmt.Errorf("检查插件 %q 未完成订单:%w", id, err)
}
if count > 0 {
return fmt.Errorf("支付插件 %q 仍有 %d 个未完成订单,暂不能卸载", id, count)
}
return nil
}
func (s *Service) refreshPaymentRegistry() {
if s.pluginRuntime == nil {
return
}
if registry := s.pluginRuntime.paymentRegistrySnapshot(); registry != nil {
s.registrationMu.Lock()
s.paymentRegistry = registry
s.registrationMu.Unlock()
}
}
func (s *Service) UninstallPluginForAdmin(actor *model.User, id string) error {
if err := s.RequireAdmin(actor); err != nil {
return err
}
if err := s.UninstallPlugin(id); err != nil {
return err
}
if err := s.repo.DeleteUserPluginStates(id); err != nil {
return fmt.Errorf("清理用户插件状态:%w", err)
}
if err := s.repo.DeletePluginPlatformState(id); err != nil {
return fmt.Errorf("清理插件平台状态:%w", err)
}
return s.appendAdminAudit(actor, "plugin.uninstall", "plugin", id, "卸载自定义插件", nil)
}