feat(platform): 插件协议系统 - 支持声明式渠道、工作流与参考媒体映射

This commit is contained in:
ddCat
2026-08-26 13:11:10 +08:00
parent 054e3bbbff
commit 2f3a813ccb
58 changed files with 2074 additions and 706 deletions

View File

@@ -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;

View File

@@ -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 = [

View File

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

View File

@@ -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 = {