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

@@ -10,7 +10,7 @@ import { canvasNodeAssetCategory } from "@/lib/canvas/canvas-node-asset";
import { isCanvasFolderNode } from "@/lib/canvas/canvas-frame";
import { resolveAddNodeMenuCommands, type AddNodeMenuContext } from "@/lib/canvas/tool-registry";
import { useThemeStore } from "@/stores/use-theme-store";
import { CanvasNodeType, type CanvasNodeData, type CanvasWorkspaceMode, type ContextMenuState, type Position } from "@/types/canvas";
import { CanvasNodeType, type CanvasNodeData, type CanvasNodeTypeId, type CanvasWorkspaceMode, type ContextMenuState, type Position } from "@/types/canvas";
type CanvasAssetCategory = NonNullable<NonNullable<CanvasNodeData["metadata"]>["assetCategory"]>;
@@ -33,7 +33,7 @@ type CanvasNodeContextMenuProps = {
canRedo: boolean;
canPaste: boolean;
onClose: () => void;
onAddNode: (type: CanvasNodeType) => void;
onAddNode: (type: CanvasNodeTypeId) => void;
onAddFolder: () => void;
onChooseStyle: () => void;
onOpenDirector: (position: Position) => void;
@@ -258,7 +258,7 @@ export function CanvasNodeContextMenu({
);
}
function AddNodeContextMenu({ parentPosition, workspaceMode, isProjectLinked, reducedMotion, onAddNode, onAddFolder, onChooseStyle, onOpenDirector, onUpload, onOpenAssets, onOpenProjectCharacters }: { parentPosition: { left: number; top: number }; workspaceMode: CanvasWorkspaceMode; isProjectLinked: boolean; reducedMotion: boolean; onAddNode: (type: CanvasNodeType) => void; onAddFolder: () => void; onChooseStyle: () => void; onOpenDirector: () => void; onUpload: () => void; onOpenAssets: () => void; onOpenProjectCharacters: () => void }) {
function AddNodeContextMenu({ parentPosition, workspaceMode, isProjectLinked, reducedMotion, onAddNode, onAddFolder, onChooseStyle, onOpenDirector, onUpload, onOpenAssets, onOpenProjectCharacters }: { parentPosition: { left: number; top: number }; workspaceMode: CanvasWorkspaceMode; isProjectLinked: boolean; reducedMotion: boolean; onAddNode: (type: CanvasNodeTypeId) => void; onAddFolder: () => void; onChooseStyle: () => void; onOpenDirector: () => void; onUpload: () => void; onOpenAssets: () => void; onOpenProjectCharacters: () => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const left = getSubmenuLeft(parentPosition.left);
const createContext: AddNodeMenuContext = {

View File

@@ -15,6 +15,7 @@ import { resourceIdFromStorageKey } from "@/services/api/resources";
import type { GenerationTask } from "@/services/api/task-center";
import { cacheResourceObjectUrl, getCachedResourceObjectUrl } from "@/services/resource-blob-cache";
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
import { getNodeDefinition } from "@/lib/canvas/node-registry";
import { createDefaultSubtitleStyle } from "@/types/timeline";
import { CanvasResourceMentionTextarea } from "./canvas-resource-mention-textarea";
import { useCanvasNodeActions } from "./canvas-node-action-context";
@@ -61,11 +62,34 @@ export function CanvasNodeContent(props: CanvasNodeContentProps) {
if (props.node.metadata?.status === "loading") return <LoadingContent node={props.node} theme={props.theme} onOpenTaskDetails={props.onOpenTaskDetails} />;
if (props.node.metadata?.status === "error") return <ErrorContent node={props.node} theme={props.theme} onRetry={props.onRetry} onReloadResource={props.onReloadResource} />;
const pluginDefinition = getNodeDefinition(props.node.type)?.plugin;
if (pluginDefinition) return <PluginCanvasNodeContent {...props} renderer={pluginDefinition.renderer} schema={pluginDefinition.schema} />;
const Renderer = nodeContentRenderers[props.node.type];
return Renderer ? <Renderer {...props} /> : <UnknownNodeContent theme={props.theme} />;
}
const nodeContentRenderers = {
function PluginCanvasNodeContent({ node, theme, renderer, schema }: CanvasNodeContentProps & { renderer: "declarative" | "sandbox"; schema: Record<string, unknown> }) {
if (renderer === "sandbox") {
return <div className="flex h-full w-full items-center justify-center p-4 text-center text-xs" style={{ color: theme.node.placeholder }}>
插件节点等待隔离运行时
</div>;
}
const data = node.metadata?.pluginData || {};
const fields = Object.keys(schema.properties && typeof schema.properties === "object" ? schema.properties as Record<string, unknown> : schema);
return <div className="flex h-full w-full flex-col gap-3 overflow-auto p-4 pt-10 text-xs" style={{ color: theme.node.text }}>
{fields.length ? fields.map((field) => <div key={field} className="flex flex-col gap-1">
<span className="font-medium opacity-60">{field}</span>
<span className="whitespace-pre-wrap break-words opacity-90">{formatPluginValue(data[field] ?? (field === "content" ? node.metadata?.content : undefined))}</span>
</div>) : <span className="whitespace-pre-wrap break-words">{node.metadata?.content || "插件节点"}</span>}
</div>;
}
function formatPluginValue(value: unknown) {
if (value === undefined || value === null || value === "") return "未设置";
return typeof value === "string" ? value : JSON.stringify(value);
}
const nodeContentRenderers: Partial<Record<string, (props: CanvasNodeContentProps) => ReactNode>> = {
[CanvasNodeType.Text]: TextContent,
[CanvasNodeType.Script]: UnknownNodeContent,
[CanvasNodeType.Skill]: SkillContent,
@@ -82,7 +106,7 @@ const nodeContentRenderers = {
[CanvasNodeType.Compare]: CompareNodeContent,
[CanvasNodeType.Chart]: ChartNodeContent,
[CanvasNodeType.ColorGrade]: ColorGradeNodeContent,
} satisfies Record<CanvasNodeType, (props: CanvasNodeContentProps) => ReactNode>;
};
function DrawingContent({ node, theme, drawingProjectId }: CanvasNodeContentProps) {
const shapeCount = node.metadata?.drawingShapeCount || 0;

View File

@@ -8,7 +8,7 @@ import { canvasThemes } from "@/lib/canvas-theme";
import { storyboardMinNodeHeight } from "@/lib/canvas/canvas-storyboard-layout";
import { resourceStorageLabel, resourceStorageLocation, resourceStorageTitle } from "@/lib/canvas/resource-storage-status";
import { useThemeStore } from "@/stores/use-theme-store";
import { CanvasNodeType, type CanvasNodeData, type Position } from "@/types/canvas";
import { CanvasNodeType, type CanvasNodeData, type CanvasNodeTypeId, type Position } from "@/types/canvas";
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
import { getNodeMinSize, shouldKeepAspectRatio } from "@/lib/canvas/node-registry";
import { CanvasNodeContent, CanvasNodeImageInfo } from "./canvas-node-content";
@@ -660,7 +660,7 @@ function NodeExternalHeader({ node, scale, active, editable, editing, draft, the
);
}
function nodeTypeIcon(type: CanvasNodeType) {
function nodeTypeIcon(type: CanvasNodeTypeId) {
if (type === CanvasNodeType.Image) return ImageIcon;
if (type === CanvasNodeType.Video) return Video;
if (type === CanvasNodeType.Audio) return Music2;

View File

@@ -13,7 +13,7 @@ import { canvasDockStyle } from "@/lib/canvas/canvas-aceternity-style";
import { canvasThemes, type CanvasBackgroundMode, type CanvasColorTheme, type CanvasTheme } from "@/lib/canvas-theme";
import { defaultToolbarPrefs, readToolbarPrefs, resolveAddNodeMenuCommands, resolveToolbarEntries, type AddNodeMenuCommand, type ToolContext, type ToolbarHandlers, type ToolbarPrefs } from "@/lib/canvas/tool-registry";
import { useThemeStore } from "@/stores/use-theme-store";
import type { CanvasNodeType, CanvasToolMode, CanvasWorkspaceMode } from "@/types/canvas";
import type { CanvasNodeTypeId, CanvasToolMode, CanvasWorkspaceMode } from "@/types/canvas";
export function CanvasToolbar({
selectedCount,
@@ -65,7 +65,7 @@ export function CanvasToolbar({
onAddFrame: () => void;
onAddFolder: () => void;
onAddDrawing: () => void;
onAddExtensionNode: (type: CanvasNodeType) => void;
onAddExtensionNode: (type: CanvasNodeTypeId) => void;
onOpenDirector: () => void;
onUndo: () => void;
onRedo: () => void;