refactor(payment): 支付插件 - 宿主仅通过 RPC 包加载并保护订单版本

This commit is contained in:
ddCat
2026-09-02 16:15:57 +08:00
parent d6bbdbe27b
commit 38e715ea3f
45 changed files with 1584 additions and 203 deletions

View File

@@ -257,6 +257,9 @@ func ValidateManifest(manifest Manifest) error {
if len(manifest.Contributes.Providers) == 0 && !hasNonProviderContribution(manifest.Contributes) {
return fmt.Errorf("plugin must declare at least one contribution")
}
if backend := strings.TrimSpace(manifest.Runtime.Backend); backend != "" && backend != "declarative" && backend != "rpc" && backend != "wasm" && backend != "trusted-backend" && !strings.HasPrefix(backend, "host:") {
return fmt.Errorf("unsupported plugin backend %q", backend)
}
if len(manifest.Contributes.Providers) == 0 {
return validatePaymentProviderContributions(manifest)
}
@@ -342,6 +345,16 @@ func validatePaymentProviderContributions(manifest Manifest) error {
if policy.MinMinutes <= 0 || policy.DefaultMinutes < policy.MinMinutes || policy.MaxMinutes < policy.DefaultMinutes {
return fmt.Errorf("payment provider contribution %q has invalid expiry policy", provider.ID)
}
for _, field := range provider.IdentityFields {
if strings.TrimSpace(field) == "" || len(field) > 80 {
return fmt.Errorf("payment provider contribution %q has invalid identity field %q", provider.ID, field)
}
}
for _, response := range []ManifestPaymentResponse{provider.NotificationSuccess, provider.NotificationFailure} {
if response.Status < 0 || response.Status > 599 {
return fmt.Errorf("payment provider contribution %q has invalid notification response status", provider.ID)
}
}
}
return nil
}

View File

@@ -34,6 +34,11 @@ func TestOfficialProtocolPackagesAreSelfContainedDeclarativePlugins(t *testing.T
if err != nil {
t.Fatalf("parse %s: %v", filepath.Base(path), err)
}
if len(pkg.Manifest.Contributes.PaymentProviders) > 0 {
// Payment packages use the yingce.payment/v1 executable ABI rather than
// the declarative AI provider contract covered by this catalog test.
continue
}
if pkg.Manifest.APIVersion != "yingce.plugin/v2" {
t.Fatalf("%s apiVersion = %q", filepath.Base(path), pkg.Manifest.APIVersion)
}

View File

@@ -104,13 +104,26 @@ func validatePluginPackagePath(name string) (string, error) {
if name == "" || strings.ContainsRune(name, '\\') || strings.HasPrefix(name, "/") || path.IsAbs(name) || path.Clean(name) != name || strings.HasPrefix(name, "../") || name == ".." {
return "", fmt.Errorf("invalid plugin package path %q", name)
}
if name == "manifest.json" || strings.HasPrefix(name, "web/") || strings.HasPrefix(name, "assets/") || strings.HasPrefix(name, "docs/") || name == "README.md" || name == "LICENSE" {
if name == "manifest.json" || strings.HasPrefix(name, "web/") || strings.HasPrefix(name, "backend/") || strings.HasPrefix(name, "assets/") || strings.HasPrefix(name, "docs/") || name == "README.md" || name == "LICENSE" {
return name, nil
}
return "", fmt.Errorf("plugin package path %q is outside the allowed package roots", name)
}
func validatePluginPackageRuntime(manifest Manifest, files map[string][]byte) error {
backend := strings.TrimSpace(manifest.Runtime.Backend)
if backend == "rpc" || backend == "wasm" {
entry := strings.TrimSpace(manifest.Runtime.BackendEntry)
if !strings.HasPrefix(entry, "backend/") || path.Clean(entry) != entry {
return fmt.Errorf("manifest.runtime.backendEntry must point inside backend/")
}
if _, ok := files[entry]; !ok {
return fmt.Errorf("manifest runtime backend entry %q is missing from package", entry)
}
if backend == "wasm" && !strings.HasSuffix(strings.ToLower(entry), ".wasm") {
return fmt.Errorf("wasm backend entry must have .wasm extension")
}
}
if strings.TrimSpace(manifest.Entry) == "" {
if strings.TrimSpace(manifest.Runtime.Web) != "" {
return fmt.Errorf("manifest.runtime.web requires manifest.entry")

View File

@@ -266,8 +266,9 @@ func (m *Manifest) UnmarshalJSON(data []byte) error {
}
type ManifestRuntime struct {
Backend string `json:"backend,omitempty"`
Web string `json:"web,omitempty"`
Backend string `json:"backend,omitempty"`
BackendEntry string `json:"backendEntry,omitempty"`
Web string `json:"web,omitempty"`
}
type ManifestConfiguration struct {
@@ -300,11 +301,14 @@ type ManifestContributions struct {
}
type ManifestPaymentProvider struct {
ID string `json:"id"`
Label string `json:"label"`
Icon string `json:"icon"`
CheckoutMode string `json:"checkoutMode"`
ExpiryPolicy ManifestPaymentExpiryPolicy `json:"expiryPolicy"`
ID string `json:"id"`
Label string `json:"label"`
Icon string `json:"icon"`
CheckoutMode string `json:"checkoutMode"`
ExpiryPolicy ManifestPaymentExpiryPolicy `json:"expiryPolicy"`
IdentityFields []string `json:"identityFields,omitempty"`
NotificationSuccess ManifestPaymentResponse `json:"notificationSuccess,omitempty"`
NotificationFailure ManifestPaymentResponse `json:"notificationFailure,omitempty"`
}
type ManifestPaymentExpiryPolicy struct {
@@ -313,6 +317,12 @@ type ManifestPaymentExpiryPolicy struct {
MaxMinutes int `json:"maxMinutes"`
}
type ManifestPaymentResponse struct {
Status int `json:"status,omitempty"`
ContentType string `json:"contentType,omitempty"`
Body string `json:"body,omitempty"`
}
type ManifestProvider struct {
ID string `json:"id"`
Label string `json:"label"`