feat(canvas): 创作助手 - 合并创作对话、能力发现与画布接续

This commit is contained in:
ddCat
2026-09-09 22:46:45 +08:00
97 changed files with 8434 additions and 326 deletions

View File

@@ -0,0 +1,24 @@
import { nanoid } from "nanoid";
import type { CanvasNodeMetadata } from "@/types/canvas";
import type { PluginAgentAction } from "./plugin-types";
export function prepareAnalysisNodeAction(nodeType: string, title: string, metadata: () => CanvasNodeMetadata): PluginAgentAction {
return {
id: "prepare-input",
description: `为已有图片准备${title}节点及连线,复用相同输入的已有节点;仅准备输入,不启动分析或收费。`,
inputSchema: { type: "object", properties: { sourceNodeId: { type: "string" } }, required: ["sourceNodeId"], additionalProperties: false },
buildOperations(input, snapshot) {
if (Object.keys(input).some((key) => key !== "sourceNodeId")) throw new Error("只接受 sourceNodeId");
const source = snapshot.nodes.find((node) => node.id === input.sourceNodeId && node.type === "image");
if (!source || !(source.metadata?.storageKey || source.metadata?.content || source.metadata?.previewContent)) throw new Error("请提供当前画布已有图片节点的真实 ID");
const existing = snapshot.nodes.find((node) => node.type === nodeType && snapshot.connections.some((edge) => edge.fromNodeId === source.id && edge.toNodeId === node.id));
if (existing) return [{ type: "select_nodes", ids: [existing.id] }];
const id = `${nodeType}-${nanoid()}`;
return [
{ type: "add_node", id, nodeType, title, position: { x: Math.max(...snapshot.nodes.map((node) => node.position.x + node.width)) + 120, y: source.position.y }, metadata: metadata() },
{ type: "connect_nodes", fromNodeId: source.id, toNodeId: id },
{ type: "select_nodes", ids: [id] },
];
},
};
}

View File

@@ -1,14 +1,15 @@
import { registerPlugin } from "@/lib/plugins/plugin-registry";
import type { PluginManifest, RegisteredPlugin } from "@/lib/plugins/plugin-types";
import { ART_CRITIQUE_NODE_TYPE, ART_CRITIQUE_PLUGIN_ID } from "@/lib/art-critique/contracts";
import { ART_CRITIQUE_NODE_TYPE, ART_CRITIQUE_PLUGIN_ID, artCritiqueSourceFingerprint, createDefaultArtCritiqueState } from "@/lib/art-critique/contracts";
import { prepareAnalysisNodeAction } from "../analysis-node-action";
const manifest: PluginManifest = {
apiVersion: "yingce.plugin/v1",
id: ART_CRITIQUE_PLUGIN_ID,
name: "AI 审美批改",
version: "0.1.0",
description: "分析图片的构图、色彩、光线和比例问题,输出结构化批改报告与可视化标注。",
description: "分析图片的构图、色彩、光线和比例问题,输出结构化批改报告与可视化标注。prepare-input 准备节点;canvas_start_art_critique 打开分析并准备真实报价,用户确认费用后执行;canvas_read_plugin_node 读取结果。",
author: "内置工具",
surfaces: ["node", "fullscreen"],
permissions: ["canvas.read", "canvas.write", "media.read", "ai.text"],
@@ -30,6 +31,15 @@ const manifest: PluginManifest = {
},
};
export const artCritiquePlugin: RegisteredPlugin = { manifest };
export const artCritiquePlugin: RegisteredPlugin = {
manifest,
agentActions: [prepareAnalysisNodeAction(ART_CRITIQUE_NODE_TYPE, "AI 审美批改", () => ({ artCritique: createDefaultArtCritiqueState() }))],
readAgentNode: (node, snapshot) => {
const state = node.metadata?.artCritique;
const sources = snapshot.nodes.filter((item) => item.type === "image" && snapshot.connections.some((edge) => edge.fromNodeId === item.id && edge.toNodeId === node.id));
const currentReport = state?.report && sources.length === 1 && state.report.sourceFingerprint === artCritiqueSourceFingerprint(sources[0]) ? state.report : undefined;
return { status: state?.report && !currentReport ? "stale" : state?.status || "idle", stage: state?.analysisStage, stageTaskIds: state?.stageTaskIds, sourceNodeId: state?.sourceNodeId, summary: currentReport?.summary, strengths: currentReport?.strengths, issues: currentReport?.issues, options: currentReport?.options, error: state?.errorMessage, execution: "已有报告仅在输入指纹一致时返回;canvas_start_art_critique 准备分析报价,用户确认后执行。" };
},
};
registerPlugin(artCritiquePlugin);

View File

@@ -1,7 +1,8 @@
import { registerPlugin } from "@/lib/plugins/plugin-registry";
import type { PluginManifest, RegisteredPlugin } from "@/lib/plugins/plugin-types";
import { PORTRAIT_CLEARANCE_NODE_TYPE, PORTRAIT_CLEARANCE_PLUGIN_ID } from "@/lib/portrait-clearance/contracts";
import { PORTRAIT_CLEARANCE_NODE_TYPE, PORTRAIT_CLEARANCE_PLUGIN_ID, createDefaultPortraitClearanceState } from "@/lib/portrait-clearance/contracts";
import { prepareAnalysisNodeAction } from "../analysis-node-action";
const manifest: PluginManifest = {
apiVersion: "yingce.plugin/v1",
@@ -35,6 +36,13 @@ const manifest: PluginManifest = {
},
};
export const portraitClearancePlugin: RegisteredPlugin = { manifest };
export const portraitClearancePlugin: RegisteredPlugin = {
manifest,
agentActions: [prepareAnalysisNodeAction(PORTRAIT_CLEARANCE_NODE_TYPE, "肖像可识别性排查", () => ({ portraitClearance: createDefaultPortraitClearanceState() }))],
readAgentNode: (node) => {
const state = node.metadata?.portraitClearance;
return { mode: state?.mode, task: state?.task, lastResult: state?.lastResult, execution: "仅返回已观测任务和结果;新分析仍通过插件自己的任务入口启动。" };
},
};
registerPlugin(portraitClearancePlugin);

View File

@@ -51,6 +51,11 @@ function assertManifestV2(manifest: PluginManifestV2) {
export function registerPlugin(plugin: RegisteredPlugin) {
assertManifest(plugin.manifest);
const actionIds = new Set<string>();
for (const action of plugin.agentActions || []) {
if (!action.id.trim() || !action.description.trim() || actionIds.has(action.id) || typeof action.buildOperations !== "function") throw new Error("插件 Agent 动作需要唯一 ID、说明和操作构建器");
actionIds.add(action.id);
}
const existing = registeredPlugins.get(plugin.manifest.id);
if (existing && existing.manifest.version !== plugin.manifest.version) {
throw new Error(`插件 ${plugin.manifest.id} 已注册其他版本`);

View File

@@ -317,7 +317,18 @@ export type AssetSourceProvider = {
openAsset?: (item: ExternalAssetItem) => Promise<void>;
};
export type PluginAgentAction = {
id: string;
description: string;
inputSchema: Record<string, unknown>;
/** Pure planner: validate input and return operations, never perform side effects here. */
buildOperations: (input: Record<string, unknown>, snapshot: import("@/lib/canvas/canvas-agent-ops").CanvasAgentSnapshot) => import("@/lib/canvas/canvas-agent-ops").CanvasAgentOp[];
};
export type RegisteredPlugin = {
agentActions?: PluginAgentAction[];
/** Pure, bounded projection: never return embedded media or provider credentials. */
readAgentNode?: (node: import("@/types/canvas").CanvasNodeData, snapshot: import("@/lib/canvas/canvas-agent-ops").CanvasAgentSnapshot) => Record<string, unknown>;
/** v1 或 v2 插件清单;v2 清单结构为 v1 超集(含 editorSlots 声明)。 */
manifest: PluginManifest | PluginManifestV2;
source?: "bundled" | "uploaded" | string;