feat(plugin): AI 审美批改 - 增加结构化视觉分析与精准标注 (#387)
* feat(portrait-clearance): 肖像排查 - 增加画布插件与本机检测链路 * fix(web): 插件中心 - 修复应用插件筛选与自动定位 * feat(plugin): AI审美批改 - 完成分析报告与问题标注闭环
This commit is contained in:
@@ -95,6 +95,7 @@ import { CanvasProjectWorldLayers } from "./canvas-project-world-layers";
|
||||
import { CanvasNodeActionContext, type CanvasNodeActionContextValue } from "@/components/canvas/canvas-node-action-context";
|
||||
import { bringCanvasNodeToFront, type CanvasNodeStackOrder } from "@/lib/canvas/canvas-node-stack-order";
|
||||
import { PortraitClearanceModal } from "@/components/canvas/portrait-clearance/portrait-clearance-modal";
|
||||
import { AiArtCritiqueModal } from "@/components/canvas/art-critique/ai-art-critique-modal";
|
||||
import { CanvasNodeGraphContext, type CanvasNodeGraphContextValue } from "@/components/canvas/canvas-node-graph-context";
|
||||
import { CanvasRefreshShell } from "./canvas-refresh-shell";
|
||||
import { queryGenerationTask } from "@/services/api/task-center";
|
||||
@@ -147,6 +148,7 @@ import type { ReferenceImage } from "@/types/image";
|
||||
import type { PortraitClearanceNodeState } from "@/lib/portrait-clearance/contracts";
|
||||
import { createDefaultPortraitClearanceState, PORTRAIT_CLEARANCE_NODE_TYPE } from "@/lib/portrait-clearance/contracts";
|
||||
import { reconcilePortraitClearanceInputBindings } from "@/lib/portrait-clearance/input-bindings";
|
||||
import { ART_CRITIQUE_NODE_TYPE } from "@/lib/art-critique/contracts";
|
||||
|
||||
const CanvasDirectorWorkbench = lazy(() => import("@/components/canvas/director/canvas-director-workbench").then((module) => ({ default: module.CanvasDirectorWorkbench })));
|
||||
const CanvasDrawingEditorModal = lazy(() => import("@/components/canvas/canvas-drawing-editor-modal").then((module) => ({ default: module.CanvasDrawingEditorModal })));
|
||||
@@ -287,6 +289,7 @@ function InfiniteCanvasPage() {
|
||||
const [previewNodeId, setPreviewNodeId] = useState<string | null>(null);
|
||||
const [scriptEditorNodeId, setScriptEditorNodeId] = useState<string | null>(null);
|
||||
const [portraitClearanceNodeId, setPortraitClearanceNodeId] = useState<string | null>(null);
|
||||
const [artCritiqueNodeId, setArtCritiqueNodeId] = useState<string | null>(null);
|
||||
const [scriptScrollTopById, setScriptScrollTopById] = useState<Record<string, number>>({});
|
||||
const [directorNodeId, setDirectorNodeId] = useState<string | null>(null);
|
||||
const [versionCompareRootId, setVersionCompareRootId] = useState<string | null>(null);
|
||||
@@ -918,6 +921,7 @@ function InfiniteCanvasPage() {
|
||||
setRunningNodeId(clearDeletedId);
|
||||
setScriptEditorNodeId(clearDeletedId);
|
||||
setPortraitClearanceNodeId(clearDeletedId);
|
||||
setArtCritiqueNodeId(clearDeletedId);
|
||||
setDirectorNodeId(clearDeletedId);
|
||||
setVersionCompareRootId(clearDeletedId);
|
||||
setScriptScrollTopById((current) => Object.fromEntries(Object.entries(current).filter(([id]) => !removedIds.has(id))));
|
||||
@@ -928,7 +932,7 @@ function InfiniteCanvasPage() {
|
||||
}
|
||||
cleanupCanvasFiles({ projectId, nodes: nextNodes, chatSessions });
|
||||
},
|
||||
[chatSessions, cleanupCanvasFiles, message, projectId, setAngleNodeId, setAnnotationNodeId, setCropNodeId, setEmotionNodeId, setFrameDialogNodeId, setMaskEditNodeId, setSegmentDialogNodeId, setSplitNodeId, setUpscaleNodeId, setRunningNodeId],
|
||||
[chatSessions, cleanupCanvasFiles, message, projectId, setAngleNodeId, setAnnotationNodeId, setArtCritiqueNodeId, setCropNodeId, setEmotionNodeId, setFrameDialogNodeId, setMaskEditNodeId, setSegmentDialogNodeId, setSplitNodeId, setUpscaleNodeId, setRunningNodeId],
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -1030,6 +1034,9 @@ function InfiniteCanvasPage() {
|
||||
} else if (node.type === PORTRAIT_CLEARANCE_NODE_TYPE) {
|
||||
setDialogNodeId(null);
|
||||
setPortraitClearanceNodeId(node.id);
|
||||
} else if (node.type === ART_CRITIQUE_NODE_TYPE) {
|
||||
setDialogNodeId(null);
|
||||
setArtCritiqueNodeId(node.id);
|
||||
} else {
|
||||
// 选择参考媒体时保留当前工作流配置面板,避免点击图片后配置“返回/消失”。
|
||||
// 没有工作流配置面板时,媒体节点仍按原逻辑打开自己的面板。
|
||||
@@ -1047,7 +1054,7 @@ function InfiniteCanvasPage() {
|
||||
|
||||
const handleNodeDragEnd = useCallback((nodeId: string) => {
|
||||
const node = nodesRef.current.find((item) => item.id === nodeId);
|
||||
if (!node || node.type === CanvasNodeType.Script || node.type === CanvasNodeType.Drawing || node.type === PORTRAIT_CLEARANCE_NODE_TYPE) {
|
||||
if (!node || node.type === CanvasNodeType.Script || node.type === CanvasNodeType.Drawing || node.type === PORTRAIT_CLEARANCE_NODE_TYPE || node.type === ART_CRITIQUE_NODE_TYPE) {
|
||||
setDialogNodeId(null);
|
||||
return;
|
||||
}
|
||||
@@ -1328,6 +1335,7 @@ function InfiniteCanvasPage() {
|
||||
const characterReferenceNode = characterReferenceNodeId ? nodeById.get(characterReferenceNodeId) || null : null;
|
||||
const drawingNode = drawingNodeId ? nodeById.get(drawingNodeId) || null : null;
|
||||
const portraitClearanceNode = portraitClearanceNodeId ? nodeById.get(portraitClearanceNodeId) || null : null;
|
||||
const artCritiqueNode = artCritiqueNodeId ? nodeById.get(artCritiqueNodeId) || null : null;
|
||||
const portraitClearanceInputs = portraitClearanceNode
|
||||
? connections
|
||||
.filter((connection) => connection.toNodeId === portraitClearanceNode.id)
|
||||
@@ -1335,6 +1343,13 @@ function InfiniteCanvasPage() {
|
||||
.map((connection) => nodeById.get(connection.fromNodeId))
|
||||
.filter((node): node is CanvasNodeData => Boolean(node))
|
||||
: [];
|
||||
const artCritiqueInputs = artCritiqueNode
|
||||
? connections
|
||||
.filter((connection) => connection.toNodeId === artCritiqueNode.id)
|
||||
.sort((left, right) => left.id.localeCompare(right.id))
|
||||
.map((connection) => nodeById.get(connection.fromNodeId))
|
||||
.filter((node): node is CanvasNodeData => Boolean(node))
|
||||
: [];
|
||||
const addPortraitCandidateToCanvas = useCallback(async (candidate: { id: string; title: string; imageArtifactId: string }, dataUrl: string) => {
|
||||
const target = portraitClearanceNodeId ? nodesRef.current.find((node) => node.id === portraitClearanceNodeId) : undefined;
|
||||
if (!target) return;
|
||||
@@ -1389,6 +1404,15 @@ function InfiniteCanvasPage() {
|
||||
setToolbarNodeId(null);
|
||||
setPortraitClearanceNodeId(node.id);
|
||||
}, []);
|
||||
const openArtCritique = useCallback((node: CanvasNodeData) => {
|
||||
if (node.type !== ART_CRITIQUE_NODE_TYPE) return;
|
||||
setSelectedNodeIds(new Set([node.id]));
|
||||
setSelectedConnectionId(null);
|
||||
setContextMenu(null);
|
||||
setDialogNodeId(null);
|
||||
setToolbarNodeId(null);
|
||||
setArtCritiqueNodeId(node.id);
|
||||
}, []);
|
||||
const duplicateNodeFromContent = useCallback((node: CanvasNodeData) => duplicateNode(node.id), [duplicateNode]);
|
||||
const deleteNodeFromContent = useCallback((node: CanvasNodeData) => deleteNodes(new Set([node.id])), [deleteNodes]);
|
||||
const updateNodeFromContent = useCallback((nodeId: string, update: (node: CanvasNodeData) => CanvasNodeData) => {
|
||||
@@ -1427,7 +1451,8 @@ function InfiniteCanvasPage() {
|
||||
updateNode: updateNodeFromContent,
|
||||
updateMediaNode: updateMediaNodeFromContent,
|
||||
openPortraitClearance,
|
||||
}), [deleteNodeFromContent, downloadNodeImage, duplicateNodeFromContent, openPortraitClearance, updateMediaNodeFromContent, updateNodeFromContent, updateNodeMetadataFromContent]);
|
||||
openArtCritique,
|
||||
}), [deleteNodeFromContent, downloadNodeImage, duplicateNodeFromContent, openArtCritique, openPortraitClearance, updateMediaNodeFromContent, updateNodeFromContent, updateNodeMetadataFromContent]);
|
||||
const { agentSnapshot, agentUndoCount, applyAgentOps, canUndoAgentOps, dismissLastAgentChange, lastAgentChange, undoAgentOps, viewLastAgentChange } = useCanvasAgentOperations({
|
||||
projectId,
|
||||
domainProjectId: currentProject?.projectId,
|
||||
@@ -1523,6 +1548,7 @@ function InfiniteCanvasPage() {
|
||||
setEmotionNodeId(null);
|
||||
setPreviewNodeId(null);
|
||||
setRunningNodeId(null);
|
||||
setArtCritiqueNodeId(null);
|
||||
deselectCanvas();
|
||||
setClearConfirmOpen(false);
|
||||
clearCanvasFiles();
|
||||
@@ -2757,12 +2783,20 @@ function InfiniteCanvasPage() {
|
||||
<PortraitClearanceModal
|
||||
projectId={projectId}
|
||||
node={portraitClearanceNode}
|
||||
upstreamNodes={portraitClearanceInputs}
|
||||
open={Boolean(portraitClearanceNode)}
|
||||
onClose={() => setPortraitClearanceNodeId(null)}
|
||||
onUpdateState={(nodeId, state: PortraitClearanceNodeState) => handleConfigNodeChange(nodeId, { portraitClearance: state })}
|
||||
onAddCandidate={addPortraitCandidateToCanvas}
|
||||
/>
|
||||
upstreamNodes={portraitClearanceInputs}
|
||||
open={Boolean(portraitClearanceNode)}
|
||||
onClose={() => setPortraitClearanceNodeId(null)}
|
||||
onUpdateState={(nodeId, state: PortraitClearanceNodeState) => handleConfigNodeChange(nodeId, { portraitClearance: state })}
|
||||
onAddCandidate={addPortraitCandidateToCanvas}
|
||||
/>
|
||||
|
||||
<AiArtCritiqueModal
|
||||
node={artCritiqueNode}
|
||||
upstreamNodes={artCritiqueInputs}
|
||||
open={Boolean(artCritiqueNode)}
|
||||
onClose={() => setArtCritiqueNodeId(null)}
|
||||
onUpdateState={(nodeId, state) => handleConfigNodeChange(nodeId, { artCritique: state })}
|
||||
/>
|
||||
|
||||
<CanvasScriptEditor
|
||||
node={activeScriptNode}
|
||||
|
||||
@@ -8,6 +8,7 @@ import "@/lib/plugins/builtin";
|
||||
import { EAGLE_PLUGIN_ID } from "@/lib/plugins/builtin/eagle";
|
||||
import { PROMPT_OPTIMIZER_PLUGIN_ID } from "@/lib/plugins/builtin/prompt-optimizer";
|
||||
import { COMFYUI_PLUGIN_ID, RUNNINGHUB_PLUGIN_ID } from "@/lib/plugins/builtin/workflows";
|
||||
import { ART_CRITIQUE_PLUGIN_ID } from "@/lib/art-critique/contracts";
|
||||
import type { PluginManifest, RegisteredPlugin } from "@/lib/plugins/plugin-types";
|
||||
import { getEagleLibrary, type EagleFolder } from "@/services/api/eagle";
|
||||
import { fetchPlugins, setUserPluginEnabled, type BackendPlugin, type PluginState } from "@/services/api/plugins";
|
||||
@@ -46,6 +47,7 @@ const permissionLabels: Record<string, string> = {
|
||||
"asset.upload": "上传素材",
|
||||
"generation.run": "调用生成",
|
||||
"ai.text": "调用已配置的文本/视觉理解模型",
|
||||
"media.read": "读取输入媒体",
|
||||
"external.open": "打开外部详情",
|
||||
};
|
||||
|
||||
@@ -617,7 +619,7 @@ function toRegisteredPlugin(plugin: BackendPlugin): RegisteredPlugin {
|
||||
}
|
||||
|
||||
function isOfficialApplicationPlugin(pluginId: string) {
|
||||
return [RUNNINGHUB_PLUGIN_ID, COMFYUI_PLUGIN_ID, EAGLE_PLUGIN_ID, PROMPT_OPTIMIZER_PLUGIN_ID, "portrait-clearance"].includes(pluginId);
|
||||
return [RUNNINGHUB_PLUGIN_ID, COMFYUI_PLUGIN_ID, EAGLE_PLUGIN_ID, PROMPT_OPTIMIZER_PLUGIN_ID, "portrait-clearance", ART_CRITIQUE_PLUGIN_ID].includes(pluginId);
|
||||
}
|
||||
|
||||
function pluginSourceLabel(plugin: RegisteredPlugin, state?: PluginState) {
|
||||
|
||||
Reference in New Issue
Block a user