feat(platform): 插件协议系统 - 支持声明式渠道、工作流与参考媒体映射
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { getDataUrlByteSize } from "@/lib/image-utils";
|
||||
import type { Asset, AssetCategory, NewAsset } from "@/stores/use-asset-store";
|
||||
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
|
||||
import { CanvasNodeType, type CanvasNodeData, type CanvasNodeTypeId } from "@/types/canvas";
|
||||
|
||||
export type CanvasAssetSource = "canvas-generation" | "canvas-upload" | "canvas-manual";
|
||||
|
||||
@@ -124,7 +124,7 @@ export function canvasNodeAssetCategory(node: CanvasNodeData): AssetCategory {
|
||||
return "other";
|
||||
}
|
||||
|
||||
function canvasAssetFallbackTitle(type: CanvasNodeType) {
|
||||
function canvasAssetFallbackTitle(type: CanvasNodeTypeId) {
|
||||
if (type === CanvasNodeType.Image) return "画布图片";
|
||||
if (type === CanvasNodeType.Video) return "画布视频";
|
||||
if (type === CanvasNodeType.Audio) return "画布音频";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
|
||||
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
|
||||
import { CanvasNodeType, isBuiltinCanvasNodeType, type CanvasNodeData } from "@/types/canvas";
|
||||
|
||||
export const MEDIA_NODE_MIN_SIZE = { width: 420, height: 236 } as const;
|
||||
export const VIDEO_NODE_MAX_SIZE = { width: 720, height: 520 } as const;
|
||||
@@ -34,10 +34,11 @@ export function ensureMediaNodeMinimumSize(node: CanvasNodeData) {
|
||||
const title = node.title === "New Generation" ? "图片" : node.title === "Video" ? "视频" : node.title;
|
||||
let width = node.width;
|
||||
let height = node.height;
|
||||
const emptyStage = NODE_DEFAULT_SIZE[node.type];
|
||||
const emptyStage = isBuiltinCanvasNodeType(node.type) ? NODE_DEFAULT_SIZE[node.type] : undefined;
|
||||
const shouldPromoteEmptyStage = !node.metadata?.content
|
||||
&& !node.metadata?.freeResize
|
||||
&& !node.metadata?.locked
|
||||
&& emptyStage !== undefined
|
||||
&& width * height < emptyStage.width * emptyStage.height;
|
||||
if (shouldPromoteEmptyStage) {
|
||||
width = emptyStage.width;
|
||||
|
||||
@@ -7,9 +7,10 @@ import type { NodeGenerationInput } from "@/components/canvas/canvas-node-genera
|
||||
import { isFrameNode } from "@/lib/canvas/canvas-frame";
|
||||
import { nodeSizeFromRatio } from "@/lib/canvas/canvas-node-size";
|
||||
import { canvasResourceMentionToken, type CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
||||
import { getNodeDefinition } from "@/lib/canvas/node-registry";
|
||||
import { scopedLocalStorage } from "@/lib/user-scope";
|
||||
import type { GenerationTask } from "@/services/api/task-center";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type CanvasNodeMetadata, type CanvasWorkspaceMode, type ConnectionHandle, type Position, type StoryboardColumn, type StoryboardRow } from "@/types/canvas";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type CanvasNodeMetadata, type CanvasNodeTypeId, type CanvasWorkspaceMode, type ConnectionHandle, type Position, type StoryboardColumn, type StoryboardRow } from "@/types/canvas";
|
||||
|
||||
const CANVAS_WORKSPACE_MODE_STORAGE_KEY = "canvas-workspace-mode-v1";
|
||||
|
||||
@@ -32,8 +33,11 @@ export function persistCanvasWorkspaceMode(mode: CanvasWorkspaceMode) {
|
||||
}
|
||||
|
||||
|
||||
export function createCanvasNode(type: CanvasNodeType, position: Position, metadata?: CanvasNodeMetadata): CanvasNodeData {
|
||||
const spec = getNodeSpec(type);
|
||||
export function createCanvasNode(type: CanvasNodeTypeId, position: Position, metadata?: CanvasNodeMetadata): CanvasNodeData {
|
||||
const builtinSpec = type in NODE_DEFAULT_SIZE ? getNodeSpec(type as CanvasNodeType) : undefined;
|
||||
const pluginDefinition = getNodeDefinition(type);
|
||||
const spec = builtinSpec || (pluginDefinition ? { width: pluginDefinition.defaultSize.width, height: pluginDefinition.defaultSize.height, title: pluginDefinition.defaultTitle, metadata: pluginDefinition.defaultMetadata } : undefined);
|
||||
if (!spec) throw new Error(`未注册的画布节点类型:${type}`);
|
||||
const id = `${type}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getNodeResourceKind } from "@/lib/canvas/node-registry";
|
||||
import { seedanceReferenceLabel } from "@/lib/seedance-video";
|
||||
import type { Skill } from "@/services/api/skills";
|
||||
import type { Asset, AssetCategory } from "@/stores/use-asset-store";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "@/types/canvas";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type CanvasNodeTypeId } from "@/types/canvas";
|
||||
|
||||
export type CanvasResourceKind = "image" | "video" | "audio" | "text" | "skill" | "character";
|
||||
|
||||
@@ -17,7 +17,7 @@ export type CanvasResourceReference = {
|
||||
storageKey?: string;
|
||||
text?: string;
|
||||
active: boolean;
|
||||
sourceType?: CanvasNodeType;
|
||||
sourceType?: CanvasNodeTypeId;
|
||||
skill?: Skill;
|
||||
assetId?: string;
|
||||
category?: AssetCategory;
|
||||
|
||||
@@ -16,8 +16,7 @@ const DEFAULT_MIN_SIZE = { width: 220, height: 160 } as const;
|
||||
* 尺寸、默认标题与初始 metadata 不在这里重复——它们的数据源是 constant/canvas.ts 的
|
||||
* NODE_SPECS,下方装配时统一读取。
|
||||
*
|
||||
* `satisfies Record<CanvasNodeType, …>` 是刻意的:往 CanvasNodeType 加成员后若不补定义,
|
||||
* 这里会编译失败,而不是在运行期悄悄回落成兜底值。
|
||||
* 内置节点保持集中定义;第三方节点通过同一 registry 以命名空间 ID 动态注册。
|
||||
*/
|
||||
const BUILTIN_NODE_TRAITS = {
|
||||
[CanvasNodeType.Image]: {
|
||||
@@ -166,7 +165,7 @@ const BUILTIN_NODE_TRAITS = {
|
||||
resourceKind: () => "image",
|
||||
inputKind: "image",
|
||||
},
|
||||
} satisfies Record<CanvasNodeType, Omit<CanvasNodeDefinition, "type" | "defaultTitle" | "defaultSize" | "defaultMetadata">>;
|
||||
} satisfies Record<string, Omit<CanvasNodeDefinition, "type" | "defaultTitle" | "defaultSize" | "defaultMetadata">>;
|
||||
|
||||
export const BUILTIN_NODE_DEFINITIONS: CanvasNodeDefinition[] = (Object.keys(BUILTIN_NODE_TRAITS) as CanvasNodeType[]).map((type) => {
|
||||
const spec = NODE_SPECS[type];
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export type { CanvasNodeDefinition, CanvasNodeInputKind } from "./node-definition";
|
||||
export { getNodeDefinition, getNodeGenerationMode, getNodeIcon, getNodeInputKind, getNodeLabel, getNodeListLabel, getNodeMinSize, getNodeOwnerId, getNodeResourceKind, listCreatableNodeDefinitions, listNodeDefinitions, registerNodeDefinitions, shouldKeepAspectRatio, unregisterNodeDefinitions } from "./node-registry";
|
||||
export { getNodeDefinition, getNodeGenerationMode, getNodeIcon, getNodeInputKind, getNodeLabel, getNodeListLabel, getNodeMinSize, getNodeOwnerId, getNodeResourceKind, listCreatableNodeDefinitions, listNodeDefinitions, registerNodeDefinitions, registerPluginCanvasNodes, shouldKeepAspectRatio, unregisterNodeDefinitions } from "./node-registry";
|
||||
import "./definitions";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import type { CanvasResourceKind } from "@/lib/canvas/canvas-resource-references";
|
||||
import type { CanvasGenerationMode, CanvasNodeData, CanvasNodeMetadata, CanvasNodeType } from "@/types/canvas";
|
||||
import type { CanvasGenerationMode, CanvasNodeData, CanvasNodeMetadata, CanvasNodeTypeId } from "@/types/canvas";
|
||||
import type { PluginCanvasNodeContribution } from "@/lib/plugins/plugin-types";
|
||||
|
||||
/** 作为上游输入被容量校验计数时归入的类别 */
|
||||
export type CanvasNodeInputKind = "image" | "video" | "audio" | "text";
|
||||
@@ -13,7 +14,7 @@ export type CanvasNodeInputKind = "image" | "video" | "audio" | "text";
|
||||
* 避免同一份知识散落到创建菜单、搜索弹窗、拉伸逻辑等多处后各自漂移。
|
||||
*/
|
||||
export type CanvasNodeDefinition = {
|
||||
type: CanvasNodeType;
|
||||
type: CanvasNodeTypeId;
|
||||
/** UI 短标签——创建菜单等处显示,如「文本」 */
|
||||
label: string;
|
||||
/** 列表/搜索标签,缺省派生为 `${label}节点` */
|
||||
@@ -45,4 +46,23 @@ export type CanvasNodeDefinition = {
|
||||
* 不设表示不参与计数(生成配置、背板)。与 resourceKind 不同,计数不看内容。
|
||||
*/
|
||||
inputKind?: CanvasNodeInputKind;
|
||||
plugin?: {
|
||||
pluginId: string;
|
||||
renderer: PluginCanvasNodeContribution["renderer"];
|
||||
schema: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
export function canvasNodeDefinitionFromPlugin(pluginId: string, contribution: PluginCanvasNodeContribution): CanvasNodeDefinition {
|
||||
return {
|
||||
type: contribution.id,
|
||||
label: contribution.label,
|
||||
icon: null,
|
||||
defaultTitle: contribution.defaultTitle,
|
||||
defaultSize: contribution.defaultSize,
|
||||
defaultMetadata: { pluginId, pluginNodeId: contribution.id, pluginData: {}, content: "" },
|
||||
minSize: { width: Math.min(contribution.defaultSize.width, 220), height: Math.min(contribution.defaultSize.height, 160) },
|
||||
showInCreateMenu: true,
|
||||
plugin: { pluginId, renderer: contribution.renderer, schema: contribution.schema },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { CanvasNodeData, CanvasNodeType } from "@/types/canvas";
|
||||
import type { CanvasNodeData, CanvasNodeTypeId } from "@/types/canvas";
|
||||
import type { PluginCanvasNodeContribution } from "@/lib/plugins/plugin-types";
|
||||
|
||||
import type { CanvasNodeDefinition } from "./node-definition";
|
||||
import { canvasNodeDefinitionFromPlugin, type CanvasNodeDefinition } from "./node-definition";
|
||||
|
||||
/** 模块级注册表 */
|
||||
const definitions = new Map<CanvasNodeType, CanvasNodeDefinition>();
|
||||
const definitions = new Map<CanvasNodeTypeId, CanvasNodeDefinition>();
|
||||
/** 节点类型 → 归属方。内置节点归属 "builtin",为后续扩展留出隔离位。 */
|
||||
const ownerByType = new Map<CanvasNodeType, string>();
|
||||
const ownerByType = new Map<CanvasNodeTypeId, string>();
|
||||
|
||||
/** 未注册类型的兜底最小尺寸——与非媒体节点的历史下限一致 */
|
||||
const FALLBACK_MIN_SIZE = { width: 220, height: 160 } as const;
|
||||
@@ -18,6 +19,11 @@ export function registerNodeDefinitions(defs: CanvasNodeDefinition[], ownerId =
|
||||
}
|
||||
}
|
||||
|
||||
/** Registers schema-driven canvas nodes from the unified plugin manifest. */
|
||||
export function registerPluginCanvasNodes(pluginId: string, nodes: PluginCanvasNodeContribution[]) {
|
||||
registerNodeDefinitions(nodes.map((node) => canvasNodeDefinitionFromPlugin(pluginId, node)), pluginId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销某归属方的全部节点定义。
|
||||
* 只删归属该 ownerId 的条目——内置节点不会被其他归属方的增删波及。
|
||||
@@ -30,11 +36,11 @@ export function unregisterNodeDefinitions(ownerId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getNodeDefinition(type: CanvasNodeType) {
|
||||
export function getNodeDefinition(type: CanvasNodeTypeId) {
|
||||
return definitions.get(type);
|
||||
}
|
||||
|
||||
export function getNodeOwnerId(type: CanvasNodeType) {
|
||||
export function getNodeOwnerId(type: CanvasNodeTypeId) {
|
||||
return ownerByType.get(type) || "builtin";
|
||||
}
|
||||
|
||||
@@ -48,23 +54,23 @@ export function listCreatableNodeDefinitions() {
|
||||
}
|
||||
|
||||
/** UI 短标签 */
|
||||
export function getNodeLabel(type: CanvasNodeType) {
|
||||
export function getNodeLabel(type: CanvasNodeTypeId) {
|
||||
return definitions.get(type)?.label || "未知节点";
|
||||
}
|
||||
|
||||
/** 列表/搜索标签,缺省派生自 label */
|
||||
export function getNodeListLabel(type: CanvasNodeType) {
|
||||
export function getNodeListLabel(type: CanvasNodeTypeId) {
|
||||
const def = definitions.get(type);
|
||||
if (!def) return "未知节点";
|
||||
return def.listLabel || `${def.label}节点`;
|
||||
}
|
||||
|
||||
export function getNodeIcon(type: CanvasNodeType) {
|
||||
export function getNodeIcon(type: CanvasNodeTypeId) {
|
||||
return definitions.get(type)?.icon ?? null;
|
||||
}
|
||||
|
||||
/** 手动拉伸的最小尺寸 */
|
||||
export function getNodeMinSize(type: CanvasNodeType) {
|
||||
export function getNodeMinSize(type: CanvasNodeTypeId) {
|
||||
return definitions.get(type)?.minSize ?? FALLBACK_MIN_SIZE;
|
||||
}
|
||||
|
||||
@@ -87,6 +93,6 @@ export function getNodeGenerationMode(node: CanvasNodeData) {
|
||||
}
|
||||
|
||||
/** 该节点作为上游输入被计数时的类别,不参与计数返回 undefined */
|
||||
export function getNodeInputKind(type: CanvasNodeType) {
|
||||
export function getNodeInputKind(type: CanvasNodeTypeId) {
|
||||
return definitions.get(type)?.inputKind;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Clapperboard, Folder, FolderOpen, Image as ImageIcon, Layers3, Music2,
|
||||
|
||||
import { getNodeIcon, getNodeLabel } from "@/lib/canvas/node-registry";
|
||||
import { registerAddNodeMenuCommands, type AddNodeMenuCommand } from "@/lib/canvas/tool-registry";
|
||||
import { CanvasNodeType } from "@/types/canvas";
|
||||
import { CanvasNodeType, type CanvasNodeTypeId } from "@/types/canvas";
|
||||
|
||||
/** 真正创建节点的命令,文案与图标统一取自节点注册表。 */
|
||||
function nodeCommand(type: CanvasNodeType, rest: Omit<AddNodeMenuCommand, "id" | "label" | "icon" | "section">): AddNodeMenuCommand {
|
||||
@@ -13,7 +13,7 @@ function nodeCommand(type: CanvasNodeType, rest: Omit<AddNodeMenuCommand, "id" |
|
||||
* 扩展节点命令:落在独立的「展示与加工」分区,不挤占上方调好的四列创作网格。
|
||||
* 创建逻辑一律是 createNode(type),所以统一走 onAddExtensionNode 而不是各开一个 handler。
|
||||
*/
|
||||
function extensionCommand(type: CanvasNodeType, defaultOrder: number): AddNodeMenuCommand {
|
||||
function extensionCommand(type: CanvasNodeTypeId, defaultOrder: number): AddNodeMenuCommand {
|
||||
return { id: type, label: getNodeLabel(type), icon: getNodeIcon(type), section: "extension", defaultOrder, run: (ctx) => ctx.handlers.onAddExtensionNode(type) };
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { MouseEvent } from "react";
|
||||
|
||||
import type { CanvasAlignmentMode } from "@/lib/canvas/canvas-layout";
|
||||
import type { CanvasBackgroundMode } from "@/lib/canvas-theme";
|
||||
import type { CanvasNodeData, CanvasNodeMetadata, CanvasNodeType, CanvasToolMode, CanvasWorkspaceMode } from "@/types/canvas";
|
||||
import type { CanvasNodeData, CanvasNodeMetadata, CanvasNodeTypeId, CanvasToolMode, CanvasWorkspaceMode } from "@/types/canvas";
|
||||
|
||||
/** 工具栏标识——每个工具栏有独立的注册表与偏好 */
|
||||
export type ToolbarId = "main" | "selection" | "node-hover" | "add-node-menu";
|
||||
@@ -43,7 +43,7 @@ export type ToolbarHandlers = {
|
||||
* 不给每种扩展节点单开一个 onAddXxx —— 那会让 ToolbarHandlers 随节点数线性膨胀,
|
||||
* 而它们的创建逻辑完全一致(都只是 createNode(type))。
|
||||
*/
|
||||
onAddExtensionNode: (type: CanvasNodeType) => void;
|
||||
onAddExtensionNode: (type: CanvasNodeTypeId) => void;
|
||||
onChooseStyle: () => void;
|
||||
onOpenDirector: () => void;
|
||||
// 主工具栏——资源
|
||||
@@ -102,7 +102,7 @@ export type ToolbarHandlers = {
|
||||
/** 工具运行时可见的上下文。工具定义通过纯函数读取状态 */
|
||||
export type ToolContext = {
|
||||
selectedCount: number;
|
||||
selectedNodeTypes: Set<CanvasNodeType>;
|
||||
selectedNodeTypes: Set<CanvasNodeTypeId>;
|
||||
selectedVideoCount: number;
|
||||
canvasTool: CanvasToolMode;
|
||||
workspaceMode: CanvasWorkspaceMode;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export type ModelProtocol = string;
|
||||
export type ProtocolCapability = "text" | "image" | "video" | "audio";
|
||||
export type ModelProtocolDefinition = { value: ModelProtocol; label: string; vendor?: string; capability: ProtocolCapability; create: string; contentType: string; poll?: string; media: string; enabled?: boolean };
|
||||
export type ModelProtocolWorkflow = { id: string; label: string; providerId: string; capability: ProtocolCapability; parameters: Array<{ name: string; type: string; required?: boolean; description?: string; values?: string[]; mapping?: string }>; defaults?: Record<string, string | number | boolean> };
|
||||
export type ModelProtocolDefinition = { value: ModelProtocol; label: string; vendor?: string; capability: ProtocolCapability; create: string; contentType: string; poll?: string; media: string; enabled?: boolean; baseUrl?: string; workflows?: ModelProtocolWorkflow[] };
|
||||
|
||||
export function protocolGroups(protocols: ModelProtocolDefinition[]) {
|
||||
return (["text", "image", "video", "audio"] as ProtocolCapability[]).map((capability) => ({ label: { text: "文本", image: "图片", video: "视频", audio: "音频" }[capability], options: protocols.filter((item) => item.capability === capability && item.enabled !== false).map((item) => ({ label: `${item.label} · ${item.create.replace(/^POST /, "")}`, value: item.value })) }));
|
||||
|
||||
@@ -250,7 +250,11 @@ export function resolveModelGenerationDefaults(
|
||||
const channel = resolveModelChannel(config, model);
|
||||
const cost = channel.modelCosts?.find((item) => item.model === modelOptionName(model));
|
||||
const isManagedModel = Boolean(cost?.logicalModelId || cost?.logicalCapabilitySpec);
|
||||
const source = (key: keyof ModelGenerationDefaults) => explicit[key] ?? (isManagedModel ? undefined : fallback[key]);
|
||||
// A channel model capability profile is an explicit per-model contract too.
|
||||
// The persisted global values are legacy defaults and must not override a
|
||||
// model's configured duration, ratio, or resolution on a new canvas node.
|
||||
const hasModelCapabilityProfile = Boolean(cost?.capabilityConfig);
|
||||
const source = (key: keyof ModelGenerationDefaults) => explicit[key] ?? (isManagedModel || hasModelCapabilityProfile ? undefined : fallback[key]);
|
||||
const profile = modelCapabilityConfigFor(config, model);
|
||||
|
||||
if (capability === "image" && profile.image) {
|
||||
|
||||
@@ -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