feat(plugin): 插件系统 - 新增插件中心与可展开配置

This commit is contained in:
QAyong
2026-08-21 19:13:34 +08:00
parent 3b706ffdcc
commit b54e001425
12 changed files with 2659 additions and 16 deletions

View 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);

View File

@@ -0,0 +1,2 @@
import "./eagle";

View 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);
}

View 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)),
};
}

View 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;
};