feat(plugin): 插件系统 - 新增插件中心与可展开配置
This commit is contained in:
22
web/src/lib/plugins/builtin/eagle.ts
Normal file
22
web/src/lib/plugins/builtin/eagle.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { registerPlugin } from "../plugin-registry";
|
||||
import type { RegisteredPlugin } from "../plugin-types";
|
||||
|
||||
export const EAGLE_PLUGIN_ID = "eagle-asset-connector";
|
||||
|
||||
export const eagleAssetPlugin: RegisteredPlugin = {
|
||||
manifest: {
|
||||
id: EAGLE_PLUGIN_ID,
|
||||
name: "Eagle 素材库",
|
||||
version: "0.1.0",
|
||||
apiVersion: "1",
|
||||
category: "asset-source",
|
||||
description: "把 Eagle 作为影策素材管理中的外部素材来源。",
|
||||
author: "影策社区",
|
||||
surfaces: ["asset-source", "hybrid"],
|
||||
permissions: ["asset.read", "asset.search", "asset.import", "asset.upload", "external.open"],
|
||||
trusted: true,
|
||||
},
|
||||
};
|
||||
|
||||
registerPlugin(eagleAssetPlugin);
|
||||
|
||||
2
web/src/lib/plugins/builtin/index.ts
Normal file
2
web/src/lib/plugins/builtin/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import "./eagle";
|
||||
|
||||
36
web/src/lib/plugins/plugin-registry.ts
Normal file
36
web/src/lib/plugins/plugin-registry.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { PluginManifest, RegisteredPlugin } from "./plugin-types";
|
||||
|
||||
const registeredPlugins = new Map<string, RegisteredPlugin>();
|
||||
|
||||
function assertManifest(manifest: PluginManifest) {
|
||||
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(manifest.id)) throw new Error("插件 ID 必须使用 kebab-case");
|
||||
if (!manifest.name.trim() || !manifest.version.trim() || !manifest.apiVersion.trim()) throw new Error("插件清单缺少名称、版本或 API 版本");
|
||||
if (!manifest.surfaces.length) throw new Error("插件至少需要声明一种界面形态");
|
||||
if (new Set(manifest.permissions).size !== manifest.permissions.length) throw new Error("插件权限不能重复");
|
||||
}
|
||||
|
||||
export function registerPlugin(plugin: RegisteredPlugin) {
|
||||
assertManifest(plugin.manifest);
|
||||
const existing = registeredPlugins.get(plugin.manifest.id);
|
||||
if (existing && existing.manifest.version !== plugin.manifest.version) {
|
||||
throw new Error(`插件 ${plugin.manifest.id} 已注册其他版本`);
|
||||
}
|
||||
registeredPlugins.set(plugin.manifest.id, plugin);
|
||||
}
|
||||
|
||||
export function unregisterPlugin(pluginId: string) {
|
||||
registeredPlugins.delete(pluginId);
|
||||
}
|
||||
|
||||
export function getRegisteredPlugin(pluginId: string) {
|
||||
return registeredPlugins.get(pluginId);
|
||||
}
|
||||
|
||||
export function listRegisteredPlugins() {
|
||||
return [...registeredPlugins.values()];
|
||||
}
|
||||
|
||||
export function listRegisteredManifests(): PluginManifest[] {
|
||||
return listRegisteredPlugins().map(({ manifest }) => manifest);
|
||||
}
|
||||
|
||||
20
web/src/lib/plugins/plugin-storage.ts
Normal file
20
web/src/lib/plugins/plugin-storage.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { localForageStorageForScope } from "@/lib/localforage-storage";
|
||||
|
||||
export function createPluginStorage(pluginId: string) {
|
||||
const storage = localForageStorageForScope();
|
||||
const keyFor = (key: string) => `infinite-canvas:plugin:${pluginId}:${key}`;
|
||||
return {
|
||||
get: async <T>(key: string) => {
|
||||
const value = await storage.getItem(keyFor(key));
|
||||
if (!value) return null;
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch {
|
||||
throw new Error(`插件 ${pluginId} 的配置数据损坏`);
|
||||
}
|
||||
},
|
||||
set: async <T>(key: string, value: T) => storage.setItem(keyFor(key), JSON.stringify(value)),
|
||||
remove: (key: string) => storage.removeItem(keyFor(key)),
|
||||
};
|
||||
}
|
||||
|
||||
91
web/src/lib/plugins/plugin-types.ts
Normal file
91
web/src/lib/plugins/plugin-types.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { Asset } from "@/stores/use-asset-store";
|
||||
|
||||
export const PLUGIN_API_VERSION = "1" as const;
|
||||
|
||||
export type PluginCategory = "asset-source" | "canvas-node" | "workflow" | "ai-capability" | "import-export" | "agent";
|
||||
export type PluginSurface = "node" | "fullscreen" | "hybrid" | "asset-source";
|
||||
export type PluginPermission =
|
||||
| "canvas.read"
|
||||
| "canvas.write"
|
||||
| "asset.read"
|
||||
| "asset.search"
|
||||
| "asset.import"
|
||||
| "asset.upload"
|
||||
| "generation.run"
|
||||
| "external.open";
|
||||
|
||||
export type PluginManifest = {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
apiVersion: string;
|
||||
category: PluginCategory;
|
||||
description: string;
|
||||
author?: string;
|
||||
entry?: string;
|
||||
surfaces: PluginSurface[];
|
||||
permissions: PluginPermission[];
|
||||
trusted?: boolean;
|
||||
};
|
||||
|
||||
export type PluginStorage = {
|
||||
get<T>(key: string): Promise<T | null>;
|
||||
set<T>(key: string, value: T): Promise<void>;
|
||||
remove(key: string): Promise<void>;
|
||||
};
|
||||
|
||||
export type PluginHostContext = {
|
||||
manifest: PluginManifest;
|
||||
permissions: ReadonlySet<PluginPermission>;
|
||||
storage: PluginStorage;
|
||||
};
|
||||
|
||||
export type AssetSourceQuery = {
|
||||
keyword?: string;
|
||||
folderId?: string;
|
||||
tags?: string[];
|
||||
kind?: Asset["kind"];
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
export type ExternalAssetItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
kind: Asset["kind"];
|
||||
thumbnailUrl?: string;
|
||||
fileUrl?: string;
|
||||
mimeType?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
bytes?: number;
|
||||
tags?: string[];
|
||||
folderId?: string;
|
||||
description?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type AssetSourceProvider = {
|
||||
list?: (query: AssetSourceQuery) => Promise<ExternalAssetItem[]>;
|
||||
importAsset?: (item: ExternalAssetItem, signal?: AbortSignal) => Promise<Asset>;
|
||||
uploadAsset?: (asset: Asset, signal?: AbortSignal) => Promise<ExternalAssetItem>;
|
||||
openAsset?: (item: ExternalAssetItem) => Promise<void>;
|
||||
};
|
||||
|
||||
export type RegisteredPlugin = {
|
||||
manifest: PluginManifest;
|
||||
activate?: (context: PluginHostContext) => Promise<void> | void;
|
||||
deactivate?: (context: PluginHostContext) => Promise<void> | void;
|
||||
createAssetSource?: (context: PluginHostContext) => AssetSourceProvider;
|
||||
};
|
||||
|
||||
export type PluginInstallation = {
|
||||
manifest: PluginManifest;
|
||||
enabled: boolean;
|
||||
config: Record<string, string | number | boolean>;
|
||||
installedAt: string;
|
||||
updatedAt: string;
|
||||
lastError?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user