feat(canvas): 创作助手 - 合并创作对话、能力发现与画布接续
This commit is contained in:
24
web/src/lib/plugins/analysis-node-action.ts
Normal file
24
web/src/lib/plugins/analysis-node-action.ts
Normal 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] },
|
||||
];
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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} 已注册其他版本`);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user