refactor(payment): 支付插件 - 宿主仅通过 RPC 包加载并保护订单版本
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"infinite-canvas/backend/internal/payment"
|
||||
"infinite-canvas/backend/internal/protocol"
|
||||
)
|
||||
|
||||
@@ -66,12 +67,13 @@ type pluginRecord struct {
|
||||
}
|
||||
|
||||
type pluginRuntime struct {
|
||||
mu sync.RWMutex
|
||||
mutationMu sync.Mutex
|
||||
registryPath string
|
||||
packageDir string
|
||||
plugins map[string]pluginRecord
|
||||
registry *protocol.Registry
|
||||
mu sync.RWMutex
|
||||
mutationMu sync.Mutex
|
||||
registryPath string
|
||||
packageDir string
|
||||
plugins map[string]pluginRecord
|
||||
registry *protocol.Registry
|
||||
paymentRegistry *payment.Registry
|
||||
}
|
||||
|
||||
type pluginRegistryRecord struct {
|
||||
@@ -136,8 +138,10 @@ func (c *pluginRuntime) bootstrapBuiltInPlugins() error {
|
||||
if strings.HasPrefix(strings.TrimSpace(pkg.Manifest.Runtime.Backend), "host:") {
|
||||
return fmt.Errorf("官方插件 %q 不能依赖 host 执行器", pkg.Manifest.Metadata.ID)
|
||||
}
|
||||
if _, err := protocol.LoadInstalledProviders(pkg.ManifestRaw, nil); err != nil {
|
||||
return fmt.Errorf("加载官方插件 %q:%w", pkg.Manifest.Metadata.ID, err)
|
||||
if len(pkg.Manifest.Contributes.PaymentProviders) == 0 {
|
||||
if _, err := protocol.LoadInstalledProviders(pkg.ManifestRaw, nil); err != nil {
|
||||
return fmt.Errorf("加载官方插件 %q:%w", pkg.Manifest.Metadata.ID, err)
|
||||
}
|
||||
}
|
||||
id := pkg.Manifest.Metadata.ID
|
||||
if _, duplicate := builtInIDs[id]; duplicate {
|
||||
@@ -165,13 +169,22 @@ func (c *pluginRuntime) bootstrapBuiltInPlugins() error {
|
||||
if record.InstalledAt.IsZero() {
|
||||
record.InstalledAt = now
|
||||
}
|
||||
record.ID, record.Raw, record.Source, record.FileName = id, manifestData, PluginOriginOfficial, entry.Name()
|
||||
source := PluginOriginOfficial
|
||||
if _, systemPayment := systemPaymentPolicies[id]; systemPayment {
|
||||
source = PluginOriginSystem
|
||||
}
|
||||
record.ID, record.Raw, record.Source, record.FileName = id, manifestData, source, entry.Name()
|
||||
record.PackagePath, record.PackageSHA256, record.UpdatedAt = packageName, hash, now
|
||||
byID[id] = record
|
||||
}
|
||||
bundledManifests := append(bundledWorkflowPluginManifests(), bundledPaymentPluginManifests()...)
|
||||
for _, bundled := range bundledManifests {
|
||||
builtInIDs[bundled.Metadata.ID] = struct{}{}
|
||||
if existing, exists := byID[bundled.Metadata.ID]; exists && existing.PackagePath != "" && isSystemPaymentPluginID(bundled.Metadata.ID) {
|
||||
// An official package is the executable source of truth. The bundled
|
||||
// manifest remains only as a fallback when the package is unavailable.
|
||||
continue
|
||||
}
|
||||
data, err := json.Marshal(bundled)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode built-in plugin %s: %w", bundled.Metadata.ID, err)
|
||||
@@ -215,6 +228,11 @@ func (c *pluginRuntime) bootstrapBuiltInPlugins() error {
|
||||
return c.writeRegistry(result)
|
||||
}
|
||||
|
||||
func isSystemPaymentPluginID(id string) bool {
|
||||
_, ok := systemPaymentPolicies[strings.TrimSpace(id)]
|
||||
return ok
|
||||
}
|
||||
|
||||
func isBuiltInPluginSource(source string) bool {
|
||||
switch strings.TrimSpace(source) {
|
||||
case "bundled", PluginOriginOfficial, PluginOriginSystem:
|
||||
@@ -295,15 +313,22 @@ func (c *pluginRuntime) reload() error {
|
||||
plugins[id] = record
|
||||
continue
|
||||
}
|
||||
if len(manifest.Contributes.PaymentProviders) > 0 {
|
||||
_, systemPayment := systemPaymentPolicies[id]
|
||||
if record.Source != PluginOriginSystem || !systemPayment {
|
||||
if len(manifest.Contributes.PaymentProviders) > 0 && record.Source == PluginOriginUploaded {
|
||||
backend := strings.TrimSpace(manifest.Runtime.Backend)
|
||||
if strings.HasPrefix(backend, "host:") {
|
||||
record.Metadata.Enabled = false
|
||||
record.Status = "invalid"
|
||||
record.Error = "首期支付插件只能使用系统宿主适配器"
|
||||
plugins[id] = record
|
||||
continue
|
||||
}
|
||||
if backend != "rpc" && backend != "wasm" {
|
||||
record.Metadata.Enabled = false
|
||||
record.Status = "invalid"
|
||||
record.Error = "上传支付插件必须声明 rpc 或 wasm 后端"
|
||||
plugins[id] = record
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(manifest.Contributes.PaymentProviders) > 0 && len(manifest.Contributes.Providers) == 0 {
|
||||
if record.Metadata.Enabled {
|
||||
@@ -349,9 +374,98 @@ func (c *pluginRuntime) reload() error {
|
||||
}
|
||||
c.plugins = plugins
|
||||
c.registry = registry
|
||||
// Payment adapters are loaded exclusively from validated plugin packages.
|
||||
// An empty registry is intentional when the official package is unavailable;
|
||||
// payment writes must fail closed instead of silently using host code.
|
||||
basePayment, registryErr := payment.NewRegistry()
|
||||
if registryErr != nil {
|
||||
return registryErr
|
||||
}
|
||||
dynamicPayments := make([]payment.Provider, 0)
|
||||
for id, record := range plugins {
|
||||
if (record.Source != PluginOriginUploaded && record.Source != PluginOriginOfficial && record.Source != PluginOriginSystem) || !record.Metadata.Enabled || record.Status != "enabled" {
|
||||
continue
|
||||
}
|
||||
var manifest protocol.Manifest
|
||||
if err := json.Unmarshal(record.Raw, &manifest); err != nil || len(manifest.Contributes.PaymentProviders) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(manifest.Runtime.Backend) != "rpc" {
|
||||
record.Status = "invalid"
|
||||
record.Error = "wasm 支付运行时尚未启用"
|
||||
plugins[id] = record
|
||||
continue
|
||||
}
|
||||
packageData, readErr := os.ReadFile(filepath.Join(c.packageDir, filepath.Base(record.PackagePath)))
|
||||
if readErr != nil {
|
||||
record.Status = "invalid"
|
||||
record.Error = "支付插件包文件不存在"
|
||||
plugins[id] = record
|
||||
continue
|
||||
}
|
||||
pkg, parseErr := protocol.ParsePluginPackage(packageData)
|
||||
if parseErr != nil {
|
||||
record.Status = "invalid"
|
||||
record.Error = parseErr.Error()
|
||||
plugins[id] = record
|
||||
continue
|
||||
}
|
||||
runtimeDir, materializeErr := materializePaymentBackend(c.packageDir, record.PackageSHA256, pkg)
|
||||
if materializeErr != nil {
|
||||
record.Status = "invalid"
|
||||
record.Error = materializeErr.Error()
|
||||
plugins[id] = record
|
||||
continue
|
||||
}
|
||||
for _, contribution := range manifest.Contributes.PaymentProviders {
|
||||
provider, providerErr := payment.NewRPCProvider(payment.DescriptorFromManifest(manifest, contribution), runtimeDir, manifest.Runtime.BackendEntry)
|
||||
if providerErr != nil {
|
||||
record.Status = "invalid"
|
||||
record.Error = providerErr.Error()
|
||||
continue
|
||||
}
|
||||
dynamicPayments = append(dynamicPayments, provider)
|
||||
}
|
||||
}
|
||||
dynamicIDs := make(map[string]struct{}, len(dynamicPayments))
|
||||
for _, provider := range dynamicPayments {
|
||||
dynamicIDs[provider.Descriptor().ID] = struct{}{}
|
||||
}
|
||||
baseProviders := make([]payment.Provider, 0)
|
||||
for _, provider := range basePayment.Providers() {
|
||||
if _, replaced := dynamicIDs[provider.Descriptor().ID]; !replaced {
|
||||
baseProviders = append(baseProviders, provider)
|
||||
}
|
||||
}
|
||||
providers := append(baseProviders, dynamicPayments...)
|
||||
c.paymentRegistry, err = payment.NewRegistry(providers...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.plugins = plugins
|
||||
return nil
|
||||
}
|
||||
|
||||
func materializePaymentBackend(packageDir, hash string, pkg protocol.PluginPackage) (string, error) {
|
||||
root := filepath.Join(packageDir, "runtime", strings.TrimSpace(hash))
|
||||
if err := os.MkdirAll(root, 0o700); err != nil {
|
||||
return "", fmt.Errorf("创建支付插件运行目录失败:%w", err)
|
||||
}
|
||||
for name, content := range pkg.Files {
|
||||
if !strings.HasPrefix(name, "backend/") || strings.HasSuffix(name, "/") {
|
||||
continue
|
||||
}
|
||||
target := filepath.Join(root, filepath.FromSlash(name))
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.WriteFile(target, content, 0o700); err != nil {
|
||||
return "", fmt.Errorf("写入支付插件运行文件失败:%w", err)
|
||||
}
|
||||
}
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func (c *pluginRuntime) list() []PluginView {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
@@ -369,6 +483,12 @@ func (c *pluginRuntime) registrySnapshot() *protocol.Registry {
|
||||
return c.registry
|
||||
}
|
||||
|
||||
func (c *pluginRuntime) paymentRegistrySnapshot() *payment.Registry {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.paymentRegistry
|
||||
}
|
||||
|
||||
func (c *pluginRuntime) install(data []byte, fileName string) (PluginView, error) {
|
||||
c.mutationMu.Lock()
|
||||
defer c.mutationMu.Unlock()
|
||||
@@ -380,19 +500,18 @@ func (c *pluginRuntime) install(data []byte, fileName string) (PluginView, error
|
||||
return PluginView{}, err
|
||||
}
|
||||
manifest := pkg.Manifest
|
||||
if len(manifest.Contributes.PaymentProviders) > 0 {
|
||||
return PluginView{}, errors.New("首期支付插件只能使用系统宿主适配器")
|
||||
}
|
||||
if strings.HasPrefix(strings.TrimSpace(manifest.Runtime.Backend), "host:") {
|
||||
return PluginView{}, errors.New("上传插件不能使用宿主内置执行器")
|
||||
}
|
||||
if _, err := protocol.LoadInstalledProviders(pkg.ManifestRaw, nil); err != nil {
|
||||
return PluginView{}, err
|
||||
if len(manifest.Contributes.PaymentProviders) == 0 {
|
||||
if _, err := protocol.LoadInstalledProviders(pkg.ManifestRaw, nil); err != nil {
|
||||
return PluginView{}, err
|
||||
}
|
||||
}
|
||||
c.mu.RLock()
|
||||
existing, exists := c.plugins[manifest.Metadata.ID]
|
||||
c.mu.RUnlock()
|
||||
if exists && isBuiltInPluginSource(existing.Source) {
|
||||
if exists && isBuiltInPluginSource(existing.Source) && !isPaymentPluginManifest(manifest) {
|
||||
return PluginView{}, fmt.Errorf("内置插件 %q 不能通过上传覆盖", manifest.Metadata.ID)
|
||||
}
|
||||
manifest.Metadata.Enabled = !exists || existing.Metadata.Enabled
|
||||
@@ -449,6 +568,11 @@ func (c *pluginRuntime) install(data []byte, fileName string) (PluginView, error
|
||||
return PluginView{}, errors.New("插件保存后未加载")
|
||||
}
|
||||
|
||||
func isPaymentPluginManifest(manifest protocol.Manifest) bool {
|
||||
backend := strings.TrimSpace(manifest.Runtime.Backend)
|
||||
return len(manifest.Contributes.PaymentProviders) > 0 && (backend == "rpc" || backend == "wasm")
|
||||
}
|
||||
|
||||
func (c *pluginRuntime) setEnabled(id string, enabled bool) (PluginView, error) {
|
||||
c.mutationMu.Lock()
|
||||
defer c.mutationMu.Unlock()
|
||||
|
||||
Reference in New Issue
Block a user