feat(platform): 插件协议系统 - 支持声明式渠道、工作流与参考媒体映射
This commit is contained in:
@@ -96,15 +96,21 @@ export const eagleAssetPlugin: RegisteredPlugin = {
|
||||
version: "0.3.0",
|
||||
publishedAt: "2026-08-21",
|
||||
updatedAt: "2026-08-22",
|
||||
apiVersion: "1",
|
||||
category: "asset-source",
|
||||
apiVersion: "yingce.plugin/v1",
|
||||
description: "把 Eagle 作为影策的外部素材来源,直接浏览原始文件夹并读写 Eagle 文件。",
|
||||
documentation: eaglePluginDocumentation,
|
||||
author: "影策社区",
|
||||
surfaces: ["asset-source"],
|
||||
permissions: ["asset.read", "asset.search", "asset.upload", "external.open"],
|
||||
trusted: true,
|
||||
configuration: { fields: ["baseUrl", "autoUploadGenerated", "generatedFolderId"] },
|
||||
configuration: {
|
||||
fields: [
|
||||
{ name: "baseUrl", type: "url", label: "Eagle Base URL", required: true, default: EAGLE_DEFAULT_BASE_URL },
|
||||
{ name: "autoUploadGenerated", type: "boolean", label: "自动上传生成结果", default: true },
|
||||
{ name: "generatedFolderId", type: "string", label: "生成结果文件夹" },
|
||||
],
|
||||
},
|
||||
runtime: { web: "trusted-backend" },
|
||||
contributes: { assetSources: ["eagle"] },
|
||||
},
|
||||
createAssetSource: ({ config }: PluginHostContext) => {
|
||||
const configuredBaseUrl = config.baseUrl;
|
||||
|
||||
@@ -59,13 +59,13 @@ const manifest: PluginManifest = {
|
||||
id: PROMPT_OPTIMIZER_PLUGIN_ID,
|
||||
name: "AI 提示词优化器",
|
||||
version: "0.3.0",
|
||||
apiVersion: "1",
|
||||
category: "ai-capability",
|
||||
apiVersion: "yingce.plugin/v1",
|
||||
description: "把模糊的生图想法整理成可执行、可比较、适配当前模型的提示词。",
|
||||
author: "影策团队",
|
||||
surfaces: ["hybrid"],
|
||||
permissions: ["canvas.read", "canvas.write", "ai.text"],
|
||||
trusted: true,
|
||||
runtime: { web: "trusted-backend" },
|
||||
contributes: { aiCapabilities: ["prompt-optimizer"] },
|
||||
};
|
||||
|
||||
const systemPrompt = [
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { PluginManifest, RegisteredPlugin } from "./plugin-types";
|
||||
import { registerPluginCanvasNodes, unregisterNodeDefinitions } from "@/lib/canvas/node-registry";
|
||||
|
||||
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 (manifest.apiVersion !== "yingce.plugin/v1") throw new Error(`不支持的插件 API 版本:${manifest.apiVersion}`);
|
||||
if (new Set(manifest.permissions).size !== manifest.permissions.length) throw new Error("插件权限不能重复");
|
||||
if (!manifest.contributes || Object.values(manifest.contributes).every((value) => !value || value.length === 0)) throw new Error("插件至少需要声明一种贡献能力");
|
||||
}
|
||||
|
||||
export function registerPlugin(plugin: RegisteredPlugin) {
|
||||
@@ -15,10 +17,14 @@ export function registerPlugin(plugin: RegisteredPlugin) {
|
||||
if (existing && existing.manifest.version !== plugin.manifest.version) {
|
||||
throw new Error(`插件 ${plugin.manifest.id} 已注册其他版本`);
|
||||
}
|
||||
if (plugin.manifest.contributes.canvasNodes?.length) {
|
||||
registerPluginCanvasNodes(plugin.manifest.id, plugin.manifest.contributes.canvasNodes);
|
||||
}
|
||||
registeredPlugins.set(plugin.manifest.id, plugin);
|
||||
}
|
||||
|
||||
export function unregisterPlugin(pluginId: string) {
|
||||
unregisterNodeDefinitions(pluginId);
|
||||
registeredPlugins.delete(pluginId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,90 @@
|
||||
import type { Asset } from "@/stores/use-asset-store";
|
||||
|
||||
export const PLUGIN_API_VERSION = "1" as const;
|
||||
export const PLUGIN_API_VERSION = "yingce.plugin/v1" as const;
|
||||
|
||||
export type PluginCategory = "asset-source" | "canvas-node" | "workflow" | "ai-capability" | "import-export" | "agent" | "protocol";
|
||||
export type PluginSurface = "node" | "fullscreen" | "hybrid" | "asset-source";
|
||||
export type PluginContributionKind = "provider" | "workflow" | "canvas-node" | "transform" | "command" | "asset-source" | "usage-observer" | "ai-capability" | "agent" | "import-export";
|
||||
export type PluginSurface = "node" | "fullscreen" | "hybrid" | "asset-source" | "settings";
|
||||
export type ProtocolCapability = "text" | "image" | "video" | "audio";
|
||||
export type ProtocolScope = "admin.system-channel" | "user.custom-channel" | "canvas" | "creation" | "agent" | string;
|
||||
export type ProtocolPluginInfo = {
|
||||
categories: ProtocolCapability[];
|
||||
scopes: ProtocolScope[];
|
||||
create?: string;
|
||||
poll?: string;
|
||||
cancel?: string;
|
||||
export type PluginRuntime = "declarative" | "sandbox" | "worker" | "trusted-backend";
|
||||
export type PluginField = {
|
||||
name: string;
|
||||
type: "string" | "number" | "boolean" | "secret" | "url" | "select" | "json";
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
default?: string | number | boolean;
|
||||
description?: string;
|
||||
values?: string[];
|
||||
};
|
||||
export type PluginParameter = {
|
||||
name: string;
|
||||
type: string;
|
||||
required?: boolean;
|
||||
description?: string;
|
||||
values?: string[];
|
||||
mapping?: string;
|
||||
};
|
||||
export type PluginProviderOperation = {
|
||||
method: "GET" | "POST" | "PUT" | "DELETE";
|
||||
path: string;
|
||||
contentType?: string;
|
||||
documentation?: string;
|
||||
parameters?: Array<{ name: string; type: string; required?: boolean; description?: string; values?: string[]; mapping?: string }>;
|
||||
fields?: Record<string, string>;
|
||||
};
|
||||
export type PluginProviderContribution = {
|
||||
id: string;
|
||||
label: string;
|
||||
capabilities: ProtocolCapability[];
|
||||
scopes: ProtocolScope[];
|
||||
baseUrl?: string;
|
||||
auth?: { type: "bearer" | "api-key" | "custom"; field: string; header?: string };
|
||||
parameters?: PluginParameter[];
|
||||
create: PluginProviderOperation;
|
||||
poll?: PluginProviderOperation;
|
||||
cancel?: PluginProviderOperation;
|
||||
response: {
|
||||
taskIdPaths?: string[];
|
||||
statusPaths?: string[];
|
||||
messagePaths?: string[];
|
||||
textPaths?: string[];
|
||||
reasoningPaths?: string[];
|
||||
resultPaths?: string[];
|
||||
resultKind?: "image" | "video" | "audio";
|
||||
resultEphemeral?: boolean;
|
||||
};
|
||||
};
|
||||
export type PluginWorkflowContribution = {
|
||||
id: string;
|
||||
label: string;
|
||||
providerId: string;
|
||||
capability: ProtocolCapability;
|
||||
parameters: PluginParameter[];
|
||||
defaults?: Record<string, string | number | boolean>;
|
||||
};
|
||||
export type PluginCanvasNodeContribution = {
|
||||
id: string;
|
||||
label: string;
|
||||
defaultTitle: string;
|
||||
defaultSize: { width: number; height: number };
|
||||
schema: Record<string, unknown>;
|
||||
renderer: "declarative" | "sandbox";
|
||||
};
|
||||
export type PluginTransformContribution = {
|
||||
id: string;
|
||||
input: "media" | "generation";
|
||||
output: "provider-request" | "media";
|
||||
runtime: PluginRuntime;
|
||||
};
|
||||
export type PluginContributions = {
|
||||
providers?: PluginProviderContribution[];
|
||||
workflows?: PluginWorkflowContribution[];
|
||||
canvasNodes?: PluginCanvasNodeContribution[];
|
||||
transforms?: PluginTransformContribution[];
|
||||
commands?: Array<{ id: string; label: string }>;
|
||||
assetSources?: string[];
|
||||
usageObservers?: string[];
|
||||
aiCapabilities?: string[];
|
||||
agents?: string[];
|
||||
importExport?: string[];
|
||||
};
|
||||
export type PluginPermission =
|
||||
| "canvas.read"
|
||||
@@ -25,28 +95,27 @@ export type PluginPermission =
|
||||
| "asset.upload"
|
||||
| "generation.run"
|
||||
| "ai.text"
|
||||
| "media.read"
|
||||
| "usage.read"
|
||||
| "external.open";
|
||||
|
||||
export type PluginManifest = {
|
||||
apiVersion: typeof PLUGIN_API_VERSION;
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
publishedAt?: string;
|
||||
updatedAt?: string;
|
||||
apiVersion: string;
|
||||
category: PluginCategory;
|
||||
description: string;
|
||||
documentation?: string;
|
||||
author?: string;
|
||||
entry?: string;
|
||||
surfaces: PluginSurface[];
|
||||
surfaces?: PluginSurface[];
|
||||
permissions: PluginPermission[];
|
||||
trusted?: boolean;
|
||||
kind?: "ui" | "protocol";
|
||||
configuration?: {
|
||||
fields: string[];
|
||||
};
|
||||
protocol?: ProtocolPluginInfo;
|
||||
configuration?: { fields: PluginField[] };
|
||||
runtime?: { backend?: PluginRuntime; web?: PluginRuntime };
|
||||
contributes: PluginContributions;
|
||||
};
|
||||
|
||||
export type PluginStorage = {
|
||||
@@ -103,6 +172,12 @@ export type PluginHostServices = {
|
||||
ai?: {
|
||||
text?: PluginAiTextService;
|
||||
};
|
||||
media?: {
|
||||
resolve: (reference: { url?: string; dataUrl?: string; kind?: string }, signal?: AbortSignal) => Promise<{ dataUrl: string; mimeType: string }>;
|
||||
};
|
||||
usage?: {
|
||||
list: (scope?: string) => Promise<ReadonlyArray<Record<string, unknown>>>;
|
||||
};
|
||||
};
|
||||
|
||||
export type PluginHostContext = {
|
||||
|
||||
Reference in New Issue
Block a user