feat(platform): 插件协议系统 - 支持声明式渠道、工作流与参考媒体映射
This commit is contained in:
@@ -17,7 +17,7 @@ import (
|
||||
"infinite-canvas/backend/internal/protocol"
|
||||
)
|
||||
|
||||
const protocolPluginMaxBytes = 512 << 10
|
||||
const protocolPluginMaxBytes = protocol.PluginManifestMaxBytes
|
||||
|
||||
// PluginView is the backend representation consumed by the single frontend
|
||||
// plugin center. Protocol-specific runtime data is nested under Protocol so
|
||||
@@ -26,6 +26,7 @@ type PluginView struct {
|
||||
Manifest PluginManifestView `json:"manifest"`
|
||||
Source string `json:"source"`
|
||||
FileName string `json:"fileName"`
|
||||
Package string `json:"package"`
|
||||
SHA256 string `json:"sha256"`
|
||||
InstalledAt time.Time `json:"installedAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
@@ -34,57 +35,65 @@ type PluginView struct {
|
||||
}
|
||||
|
||||
type PluginManifestView struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
APIVersion string `json:"apiVersion"`
|
||||
Category string `json:"category"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
Surfaces []string `json:"surfaces"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Trusted bool `json:"trusted"`
|
||||
Kind string `json:"kind"`
|
||||
Protocol PluginProtocolView `json:"protocol"`
|
||||
}
|
||||
|
||||
type PluginProtocolView struct {
|
||||
Categories []string `json:"categories"`
|
||||
Scopes []string `json:"scopes"`
|
||||
Create string `json:"create,omitempty"`
|
||||
Poll string `json:"poll,omitempty"`
|
||||
Cancel string `json:"cancel,omitempty"`
|
||||
ContentType string `json:"contentType,omitempty"`
|
||||
Documentation string `json:"documentation,omitempty"`
|
||||
Parameters []protocol.Parameter `json:"parameters,omitempty"`
|
||||
APIVersion string `json:"apiVersion"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Entry string `json:"entry,omitempty"`
|
||||
Surfaces []string `json:"surfaces,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Documentation string `json:"documentation,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Trusted bool `json:"trusted"`
|
||||
Runtime protocol.ManifestRuntime `json:"runtime,omitempty"`
|
||||
Configuration protocol.ManifestConfiguration `json:"configuration,omitempty"`
|
||||
Contributes protocol.ManifestContributions `json:"contributes"`
|
||||
}
|
||||
|
||||
type pluginRecord struct {
|
||||
Raw []byte
|
||||
Metadata protocol.Metadata
|
||||
Source string
|
||||
FileName string
|
||||
SHA256 string
|
||||
InstalledAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Status string
|
||||
Error string
|
||||
Raw []byte
|
||||
Metadata protocol.Metadata
|
||||
Source string
|
||||
FileName string
|
||||
PackagePath string
|
||||
PackageSHA256 string
|
||||
SHA256 string
|
||||
InstalledAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Status string
|
||||
Error string
|
||||
}
|
||||
|
||||
type pluginRuntime struct {
|
||||
mu sync.RWMutex
|
||||
mutationMu sync.Mutex
|
||||
dir 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
|
||||
}
|
||||
|
||||
type pluginRegistryRecord struct {
|
||||
ID string `json:"id"`
|
||||
Raw json.RawMessage `json:"manifest"`
|
||||
Source string `json:"source"`
|
||||
FileName string `json:"fileName,omitempty"`
|
||||
PackagePath string `json:"packagePath,omitempty"`
|
||||
PackageSHA256 string `json:"packageSha256,omitempty"`
|
||||
InstalledAt time.Time `json:"installedAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func newPluginRuntime(dataDir string) (*pluginRuntime, error) {
|
||||
dir := filepath.Join(dataDir, "plugins")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("create plugin directory: %w", err)
|
||||
if err := os.MkdirAll(dataDir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("create plugin registry directory: %w", err)
|
||||
}
|
||||
center := &pluginRuntime{dir: dir, plugins: make(map[string]pluginRecord)}
|
||||
packageDir := filepath.Join(dataDir, "plugin-packages")
|
||||
if err := os.MkdirAll(packageDir, 0o700); err != nil {
|
||||
return nil, fmt.Errorf("create plugin package directory: %w", err)
|
||||
}
|
||||
center := &pluginRuntime{registryPath: filepath.Join(dataDir, "plugin_registry.json"), packageDir: packageDir, plugins: make(map[string]pluginRecord)}
|
||||
if err := center.bootstrapBundledPlugins(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -95,92 +104,122 @@ func newPluginRuntime(dataDir string) (*pluginRuntime, error) {
|
||||
}
|
||||
|
||||
func (c *pluginRuntime) bootstrapBundledPlugins() error {
|
||||
stored, err := c.readRegistry()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
byID := make(map[string]pluginRegistryRecord, len(stored))
|
||||
for _, record := range stored {
|
||||
byID[record.ID] = record
|
||||
}
|
||||
items := protocol.Builtins().List("", "", true)
|
||||
bundledIDs := make(map[string]struct{}, len(items))
|
||||
for _, metadata := range items {
|
||||
bundledIDs[metadata.ID] = struct{}{}
|
||||
protocol.AttachDocumentation(&metadata)
|
||||
metadata.Execution = "host:" + metadata.ID
|
||||
metadata.Installable = true
|
||||
path := filepath.Join(c.dir, metadata.ID+".json")
|
||||
existing, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
manifest, declarative := protocol.BundledManifest(metadata.ID)
|
||||
existing := []byte(nil)
|
||||
if record, ok := byID[metadata.ID]; ok {
|
||||
existing = record.Raw
|
||||
var installed protocol.Manifest
|
||||
if err := json.Unmarshal(existing, &installed); err != nil {
|
||||
return fmt.Errorf("decode bundled protocol %s: %w", metadata.ID, err)
|
||||
return fmt.Errorf("decode bundled plugin %s: %w", metadata.ID, err)
|
||||
}
|
||||
if installed.Metadata.Execution != metadata.Execution {
|
||||
expectedBackend := "host:" + metadata.ID
|
||||
if declarative {
|
||||
expectedBackend = "declarative"
|
||||
}
|
||||
if installed.Runtime.Backend != expectedBackend {
|
||||
return fmt.Errorf("protocol id %q is reserved by a bundled plugin", metadata.ID)
|
||||
}
|
||||
if metadata.Enabled {
|
||||
metadata.Enabled = installed.Metadata.Enabled
|
||||
}
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
manifest := protocol.Manifest{
|
||||
APIVersion: "v1",
|
||||
Metadata: metadata,
|
||||
Create: protocol.ManifestOperation{Method: "POST", Path: "/__host__/" + metadata.ID},
|
||||
Response: protocol.ManifestResponse{},
|
||||
if declarative {
|
||||
manifest.Metadata.Enabled = metadata.Enabled
|
||||
manifest.Metadata.Installable = true
|
||||
} else {
|
||||
metadata.Execution = "host:" + metadata.ID
|
||||
manifest = protocol.Manifest{
|
||||
APIVersion: "yingce.plugin/v1",
|
||||
Metadata: metadata,
|
||||
Runtime: protocol.ManifestRuntime{Backend: "host:" + metadata.ID},
|
||||
Permissions: []string{"generation.run"},
|
||||
Contributes: protocol.ManifestContributions{Providers: []protocol.ManifestProvider{{
|
||||
ID: metadata.ID, Label: metadata.Name, Capabilities: metadata.Categories, Scopes: metadata.Scopes,
|
||||
Parameters: metadata.Parameters, Create: protocol.ManifestOperation{Method: "POST", Path: "/__host__/" + metadata.ID}, Response: protocol.ManifestResponse{},
|
||||
}}},
|
||||
}
|
||||
}
|
||||
data, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode bundled protocol %s: %w", metadata.ID, err)
|
||||
}
|
||||
if bytes.Equal(existing, data) {
|
||||
continue
|
||||
}
|
||||
if err := writePluginFile(path, data); err != nil {
|
||||
return fmt.Errorf("install bundled protocol %s: %w", metadata.ID, err)
|
||||
record := byID[metadata.ID]
|
||||
if !bytes.Equal(record.Raw, data) {
|
||||
now := time.Now().UTC()
|
||||
if record.InstalledAt.IsZero() {
|
||||
record.InstalledAt = now
|
||||
}
|
||||
record.UpdatedAt = now
|
||||
}
|
||||
record.ID, record.Raw, record.Source, record.PackagePath = metadata.ID, data, "bundled", ""
|
||||
byID[metadata.ID] = record
|
||||
}
|
||||
return nil
|
||||
result := make([]pluginRegistryRecord, 0, len(byID))
|
||||
for _, record := range byID {
|
||||
if record.Source == "bundled" {
|
||||
if _, exists := bundledIDs[record.ID]; !exists {
|
||||
// Bundled records are derived from the current host registry. Dropping
|
||||
// an obsolete record prevents a removed built-in protocol from surviving
|
||||
// as a second, stale provider after a host upgrade.
|
||||
continue
|
||||
}
|
||||
}
|
||||
result = append(result, record)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool { return result[i].ID < result[j].ID })
|
||||
return c.writeRegistry(result)
|
||||
}
|
||||
|
||||
func (c *pluginRuntime) reload() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
entries, err := os.ReadDir(c.dir)
|
||||
stored, err := c.readRegistry()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plugins := make(map[string]pluginRecord)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(c.dir, entry.Name())
|
||||
data, readErr := os.ReadFile(path)
|
||||
if readErr != nil {
|
||||
return fmt.Errorf("read protocol plugin %s: %w", entry.Name(), readErr)
|
||||
}
|
||||
for _, storedRecord := range stored {
|
||||
data := storedRecord.Raw
|
||||
if len(data) > protocolPluginMaxBytes {
|
||||
return fmt.Errorf("protocol plugin %s exceeds %d bytes", entry.Name(), protocolPluginMaxBytes)
|
||||
return fmt.Errorf("plugin %s exceeds %d bytes", storedRecord.ID, protocolPluginMaxBytes)
|
||||
}
|
||||
var manifest protocol.Manifest
|
||||
if err := json.Unmarshal(data, &manifest); err != nil {
|
||||
return fmt.Errorf("decode protocol plugin %s: %w", entry.Name(), err)
|
||||
return fmt.Errorf("decode plugin %s: %w", storedRecord.ID, err)
|
||||
}
|
||||
metadata := manifest.Metadata
|
||||
if strings.TrimSpace(metadata.ID) == "" {
|
||||
return fmt.Errorf("protocol plugin %s has no metadata id", entry.Name())
|
||||
return fmt.Errorf("plugin %s has no metadata id", storedRecord.ID)
|
||||
}
|
||||
if _, exists := plugins[metadata.ID]; exists {
|
||||
return fmt.Errorf("duplicate installed protocol %q", metadata.ID)
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
stat, _ := os.Stat(path)
|
||||
updated := now
|
||||
if stat != nil {
|
||||
updated = stat.ModTime().UTC()
|
||||
packageSHA256 := storedRecord.PackageSHA256
|
||||
if packageSHA256 == "" {
|
||||
packageSHA256 = pluginHash(data)
|
||||
}
|
||||
plugins[metadata.ID] = pluginRecord{Raw: data, Metadata: metadata, Source: pluginSource(metadata), FileName: entry.Name(), SHA256: pluginHash(data), InstalledAt: updated, UpdatedAt: updated, Status: "invalid"}
|
||||
plugins[metadata.ID] = pluginRecord{Raw: data, Metadata: metadata, Source: storedRecord.Source, FileName: storedRecord.FileName, PackagePath: storedRecord.PackagePath, PackageSHA256: packageSHA256, SHA256: packageSHA256, InstalledAt: storedRecord.InstalledAt, UpdatedAt: storedRecord.UpdatedAt, Status: "invalid"}
|
||||
}
|
||||
registry, err := protocol.NewRegistry()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for id, record := range plugins {
|
||||
adapter, loadErr := protocol.LoadInstalledPlugin(record.Raw, func(execution string) (protocol.Adapter, bool) {
|
||||
adapters, loadErr := protocol.LoadInstalledProviders(record.Raw, func(execution string) (protocol.Adapter, bool) {
|
||||
return protocol.Builtins().Resolve(execution)
|
||||
})
|
||||
if loadErr != nil {
|
||||
@@ -193,12 +232,22 @@ func (c *pluginRuntime) reload() error {
|
||||
}
|
||||
if !record.Metadata.Enabled {
|
||||
record.Status = "disabled"
|
||||
_ = registry.Register(protocol.UnavailableAdapter{Info: record.Metadata})
|
||||
for _, adapter := range adapters {
|
||||
info := adapter.Metadata()
|
||||
info.Enabled = false
|
||||
_ = registry.Register(protocol.UnavailableAdapter{Info: info})
|
||||
}
|
||||
plugins[id] = record
|
||||
continue
|
||||
}
|
||||
if err := registry.Register(adapter); err != nil {
|
||||
record.Error = err.Error()
|
||||
registrationFailed := false
|
||||
for _, adapter := range adapters {
|
||||
if err := registry.Register(adapter); err != nil {
|
||||
record.Error = err.Error()
|
||||
registrationFailed = true
|
||||
}
|
||||
}
|
||||
if registrationFailed {
|
||||
plugins[id] = record
|
||||
continue
|
||||
}
|
||||
@@ -215,7 +264,7 @@ func (c *pluginRuntime) list() []PluginView {
|
||||
defer c.mu.RUnlock()
|
||||
items := make([]PluginView, 0, len(c.plugins))
|
||||
for _, item := range c.plugins {
|
||||
items = append(items, PluginView{Manifest: pluginManifestView(item.Metadata, item.Source), Source: item.Source, FileName: item.FileName, SHA256: item.SHA256, InstalledAt: item.InstalledAt, UpdatedAt: item.UpdatedAt, Status: item.Status, Error: item.Error})
|
||||
items = append(items, PluginView{Manifest: pluginManifestView(item.Raw, item.Metadata, item.Source), Source: item.Source, FileName: item.FileName, Package: protocol.PluginPackageFormat, SHA256: item.SHA256, InstalledAt: item.InstalledAt, UpdatedAt: item.UpdatedAt, Status: item.Status, Error: item.Error})
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool { return items[i].Manifest.ID < items[j].Manifest.ID })
|
||||
return items
|
||||
@@ -227,37 +276,75 @@ func (c *pluginRuntime) registrySnapshot() *protocol.Registry {
|
||||
return c.registry
|
||||
}
|
||||
|
||||
func (c *pluginRuntime) install(data []byte) (PluginView, error) {
|
||||
func (c *pluginRuntime) install(data []byte, fileName string) (PluginView, error) {
|
||||
c.mutationMu.Lock()
|
||||
defer c.mutationMu.Unlock()
|
||||
if len(data) == 0 || len(data) > protocolPluginMaxBytes {
|
||||
return PluginView{}, fmt.Errorf("plugin manifest must be between 1 and %d bytes", protocolPluginMaxBytes)
|
||||
if len(data) == 0 || len(data) > protocol.PluginPackageMaxBytes {
|
||||
return PluginView{}, fmt.Errorf("plugin package must be between 1 and %d bytes", protocol.PluginPackageMaxBytes)
|
||||
}
|
||||
var manifest protocol.Manifest
|
||||
if err := json.Unmarshal(data, &manifest); err != nil {
|
||||
return PluginView{}, fmt.Errorf("插件清单 JSON 无效:%w", err)
|
||||
}
|
||||
if strings.HasPrefix(strings.TrimSpace(manifest.Metadata.Execution), "host:") {
|
||||
return PluginView{}, errors.New("上传插件不能使用宿主内置执行器")
|
||||
}
|
||||
if _, err := protocol.LoadManifest(data); err != nil {
|
||||
return PluginView{}, err
|
||||
}
|
||||
c.mu.RLock()
|
||||
_, exists := c.plugins[manifest.Metadata.ID]
|
||||
c.mu.RUnlock()
|
||||
if exists {
|
||||
return PluginView{}, fmt.Errorf("插件 ID %q 已存在;当前上传入口不支持覆盖安装", manifest.Metadata.ID)
|
||||
}
|
||||
manifest.Metadata.Enabled = true
|
||||
data, err := json.Marshal(manifest)
|
||||
pkg, err := protocol.ParsePluginPackage(data)
|
||||
if err != nil {
|
||||
return PluginView{}, err
|
||||
}
|
||||
path := filepath.Join(c.dir, manifest.Metadata.ID+".json")
|
||||
if err := c.persistAndReload(path, data, nil); err != nil {
|
||||
manifest := pkg.Manifest
|
||||
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
|
||||
}
|
||||
c.mu.RLock()
|
||||
existing, exists := c.plugins[manifest.Metadata.ID]
|
||||
c.mu.RUnlock()
|
||||
if exists && existing.Source == "bundled" {
|
||||
return PluginView{}, fmt.Errorf("内置插件 %q 不能通过上传覆盖", manifest.Metadata.ID)
|
||||
}
|
||||
manifest.Metadata.Enabled = !exists || existing.Metadata.Enabled
|
||||
manifestData, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
return PluginView{}, err
|
||||
}
|
||||
hash := pluginHash(data)
|
||||
packageName := filepath.Base(strings.TrimSpace(fileName))
|
||||
if packageName == "." || packageName == "" || packageName == string(filepath.Separator) {
|
||||
packageName = manifest.Metadata.ID + ".yingce-plugin"
|
||||
}
|
||||
packagePath := filepath.Join(c.packageDir, hash+".yingce-plugin")
|
||||
if err := writePluginFile(packagePath, data); err != nil {
|
||||
return PluginView{}, fmt.Errorf("保存插件包失败:%w", err)
|
||||
}
|
||||
stored, err := c.readRegistry()
|
||||
if err != nil {
|
||||
_ = os.Remove(packagePath)
|
||||
return PluginView{}, err
|
||||
}
|
||||
previousStored := append([]pluginRegistryRecord(nil), stored...)
|
||||
now := time.Now().UTC()
|
||||
newRecord := pluginRegistryRecord{ID: manifest.Metadata.ID, Raw: manifestData, Source: "uploaded", FileName: packageName, PackagePath: filepath.Base(packagePath), PackageSHA256: hash, InstalledAt: now, UpdatedAt: now}
|
||||
if exists {
|
||||
newRecord.InstalledAt = existing.InstalledAt
|
||||
for index := range stored {
|
||||
if stored[index].ID == manifest.Metadata.ID {
|
||||
stored[index] = newRecord
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stored = append(stored, newRecord)
|
||||
}
|
||||
if err := c.writeRegistry(stored); err != nil {
|
||||
_ = os.Remove(packagePath)
|
||||
return PluginView{}, fmt.Errorf("保存插件失败:%w", err)
|
||||
}
|
||||
if err := c.reload(); err != nil {
|
||||
_ = c.writeRegistry(previousStored)
|
||||
_ = c.reload()
|
||||
_ = os.Remove(packagePath)
|
||||
return PluginView{}, err
|
||||
}
|
||||
if exists && existing.PackagePath != "" && existing.PackagePath != filepath.Base(packagePath) {
|
||||
_ = os.Remove(filepath.Join(c.packageDir, filepath.Base(existing.PackagePath)))
|
||||
}
|
||||
for _, item := range c.list() {
|
||||
if item.Manifest.ID == manifest.Metadata.ID {
|
||||
return item, nil
|
||||
@@ -284,7 +371,20 @@ func (c *pluginRuntime) setEnabled(id string, enabled bool) (PluginView, error)
|
||||
if err != nil {
|
||||
return PluginView{}, err
|
||||
}
|
||||
if err := c.persistAndReload(filepath.Join(c.dir, record.FileName), data, record.Raw); err != nil {
|
||||
stored, err := c.readRegistry()
|
||||
if err != nil {
|
||||
return PluginView{}, err
|
||||
}
|
||||
for index := range stored {
|
||||
if stored[index].ID == record.Metadata.ID {
|
||||
stored[index].Raw = data
|
||||
stored[index].UpdatedAt = time.Now().UTC()
|
||||
}
|
||||
}
|
||||
if err := c.writeRegistry(stored); err != nil {
|
||||
return PluginView{}, err
|
||||
}
|
||||
if err := c.reload(); err != nil {
|
||||
return PluginView{}, err
|
||||
}
|
||||
for _, item := range c.list() {
|
||||
@@ -295,7 +395,9 @@ func (c *pluginRuntime) setEnabled(id string, enabled bool) (PluginView, error)
|
||||
return PluginView{}, errors.New("插件状态更新后未加载")
|
||||
}
|
||||
|
||||
func pluginManifestView(metadata protocol.Metadata, source string) PluginManifestView {
|
||||
func pluginManifestView(raw []byte, metadata protocol.Metadata, source string) PluginManifestView {
|
||||
var manifest protocol.Manifest
|
||||
_ = json.Unmarshal(raw, &manifest)
|
||||
if source == "bundled" {
|
||||
if adapter, ok := protocol.Builtins().Get(metadata.ID); ok {
|
||||
current := adapter.Metadata()
|
||||
@@ -304,20 +406,14 @@ func pluginManifestView(metadata protocol.Metadata, source string) PluginManifes
|
||||
}
|
||||
protocol.AttachDocumentation(&metadata)
|
||||
}
|
||||
categories := make([]string, 0, len(metadata.Categories))
|
||||
for _, item := range metadata.Categories {
|
||||
categories = append(categories, string(item))
|
||||
}
|
||||
scopes := make([]string, 0, len(metadata.Scopes))
|
||||
for _, item := range metadata.Scopes {
|
||||
scopes = append(scopes, string(item))
|
||||
if manifest.Metadata.ID == "" {
|
||||
manifest.Metadata = metadata
|
||||
}
|
||||
return PluginManifestView{
|
||||
ID: metadata.ID, Name: metadata.Name, Version: metadata.Version, APIVersion: "v1",
|
||||
Category: "protocol", Description: metadata.Description, Author: metadata.Vendor,
|
||||
Surfaces: []string{"hybrid"}, Permissions: []string{"generation.run"},
|
||||
Trusted: source == "bundled", Kind: "protocol",
|
||||
Protocol: PluginProtocolView{Categories: categories, Scopes: scopes, Create: metadata.Create, Poll: metadata.Poll, Cancel: metadata.Cancel, ContentType: metadata.ContentType, Documentation: metadata.Documentation, Parameters: metadata.Parameters},
|
||||
ID: metadata.ID, Name: metadata.Name, Version: metadata.Version, APIVersion: "yingce.plugin/v1", Entry: manifest.Entry, Surfaces: manifest.Surfaces,
|
||||
Description: metadata.Description, Documentation: metadata.Documentation, Author: metadata.Vendor,
|
||||
Permissions: manifest.Permissions, Trusted: source == "bundled", Runtime: manifest.Runtime,
|
||||
Configuration: manifest.Configuration, Contributes: manifest.Contributes,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,37 +424,57 @@ func (c *pluginRuntime) uninstall(id string) error {
|
||||
record, ok := c.plugins[strings.TrimSpace(id)]
|
||||
c.mu.RUnlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("协议插件 %q 不存在", id)
|
||||
return fmt.Errorf("插件 %q 不存在", id)
|
||||
}
|
||||
if record.Source == "bundled" {
|
||||
return fmt.Errorf("内置协议插件 %q 不能卸载,可停用该插件", id)
|
||||
return fmt.Errorf("内置插件 %q 不能卸载,可停用该插件", id)
|
||||
}
|
||||
path := filepath.Join(c.dir, record.FileName)
|
||||
if err := os.Remove(path); err != nil {
|
||||
stored, err := c.readRegistry()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filtered := stored[:0]
|
||||
for _, item := range stored {
|
||||
if item.ID != record.Metadata.ID {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
if err := c.writeRegistry(filtered); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.reload(); err != nil {
|
||||
_ = writePluginFile(path, record.Raw)
|
||||
_ = c.reload()
|
||||
return err
|
||||
}
|
||||
if record.PackagePath != "" {
|
||||
_ = os.Remove(filepath.Join(c.packageDir, filepath.Base(record.PackagePath)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *pluginRuntime) persistAndReload(path string, data, rollback []byte) error {
|
||||
if err := writePluginFile(path, data); err != nil {
|
||||
func (c *pluginRuntime) readRegistry() ([]pluginRegistryRecord, error) {
|
||||
data, err := os.ReadFile(c.registryPath)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return []pluginRegistryRecord{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) > protocolPluginMaxBytes*64 {
|
||||
return nil, fmt.Errorf("插件 registry 超过大小限制")
|
||||
}
|
||||
var records []pluginRegistryRecord
|
||||
if err := json.Unmarshal(data, &records); err != nil {
|
||||
return nil, fmt.Errorf("读取插件 registry 失败:%w", err)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (c *pluginRuntime) writeRegistry(records []pluginRegistryRecord) error {
|
||||
data, err := json.MarshalIndent(records, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.reload(); err != nil {
|
||||
if rollback == nil {
|
||||
_ = os.Remove(path)
|
||||
} else {
|
||||
_ = writePluginFile(path, rollback)
|
||||
}
|
||||
_ = c.reload()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return writePluginFile(c.registryPath, data)
|
||||
}
|
||||
|
||||
func writePluginFile(path string, data []byte) error {
|
||||
@@ -387,7 +503,7 @@ func writePluginFile(path string, data []byte) error {
|
||||
}
|
||||
|
||||
func pluginSource(metadata protocol.Metadata) string {
|
||||
if strings.HasPrefix(strings.TrimSpace(metadata.Execution), "host:") {
|
||||
if _, ok := protocol.Builtins().Get(metadata.ID); ok {
|
||||
return "bundled"
|
||||
}
|
||||
return "uploaded"
|
||||
|
||||
Reference in New Issue
Block a user