feat(plugin): AI 审美批改 - 增加结构化视觉分析与精准标注 (#387)
* feat(portrait-clearance): 肖像排查 - 增加画布插件与本机检测链路 * fix(web): 插件中心 - 修复应用插件筛选与自动定位 * feat(plugin): AI审美批改 - 完成分析报告与问题标注闭环
This commit is contained in:
1 parent
c69320f325
commit
6d0e7a5228
29 files changed
+4909
-15
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { maxModelInputCapacity, type ModelInputSummary } from "@/lib/model-selection";
|
||||
import { getNodeGenerationMode, getNodeInputKind } from "@/lib/canvas/node-registry";
|
||||
import { getNodeAcceptedInputKind, getNodeGenerationMode, getNodeInputKind } from "@/lib/canvas/node-registry";
|
||||
import type { AiConfig } from "@/stores/use-config-store";
|
||||
import { type CanvasConnection, type CanvasNodeData } from "@/types/canvas";
|
||||
|
||||
@@ -12,6 +12,12 @@ type CanvasConnectionPolicyOptions = {
|
||||
export function canvasConnectionError(config: AiConfig, nodes: CanvasNodeData[], connections: CanvasConnection[], candidate: ConnectionCandidate, options: CanvasConnectionPolicyOptions = {}) {
|
||||
const target = nodes.find((node) => node.id === candidate.toNodeId);
|
||||
if (!target) return "找不到连线目标节点";
|
||||
const acceptedInputKind = getNodeAcceptedInputKind(target.type);
|
||||
if (acceptedInputKind) {
|
||||
const source = nodes.find((node) => node.id === candidate.fromNodeId);
|
||||
const sourceKind = source ? getNodeInputKind(source.type) : undefined;
|
||||
if (sourceKind !== acceptedInputKind) return `${acceptedInputKindLabel(acceptedInputKind)}节点只接受${acceptedInputKindLabel(acceptedInputKind)}输入`;
|
||||
}
|
||||
const mode = getNodeGenerationMode(target);
|
||||
if (!mode) return "";
|
||||
const input = connectionInputSummary(target.id, nodes, connections, candidate);
|
||||
@@ -31,6 +37,13 @@ export function canvasConnectionError(config: AiConfig, nodes: CanvasNodeData[],
|
||||
return "";
|
||||
}
|
||||
|
||||
function acceptedInputKindLabel(kind: "image" | "video" | "audio" | "text") {
|
||||
if (kind === "image") return "图片";
|
||||
if (kind === "video") return "视频";
|
||||
if (kind === "audio") return "音频";
|
||||
return "文本";
|
||||
}
|
||||
|
||||
export function connectionInputSummary(targetNodeId: string, nodes: CanvasNodeData[], connections: CanvasConnection[], candidate?: ConnectionCandidate): ModelInputSummary {
|
||||
const sourceIds = new Set([...connections, ...(candidate ? [{ id: "candidate", ...candidate }] : [])].filter((connection) => connection.toNodeId === targetNodeId).map((connection) => connection.fromNodeId));
|
||||
const input: ModelInputSummary = { textCount: 0, imageCount: 0, videoCount: 0, audioCount: 0, characterCount: 0 };
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export type { CanvasNodeDefinition, CanvasNodeInputKind } from "./node-definition";
|
||||
export { getNodeDefinition, getNodeGenerationMode, getNodeIcon, getNodeInputKind, getNodeLabel, getNodeListLabel, getNodeMinSize, getNodeOwnerId, getNodeResourceKind, listCreatableNodeDefinitions, listNodeDefinitions, registerNodeDefinitions, registerPluginCanvasNodes, shouldKeepAspectRatio, unregisterNodeDefinitions } from "./node-registry";
|
||||
export { getNodeAcceptedInputKind, getNodeDefinition, getNodeGenerationMode, getNodeIcon, getNodeInputKind, getNodeLabel, getNodeListLabel, getNodeMinSize, getNodeOwnerId, getNodeResourceKind, listCreatableNodeDefinitions, listNodeDefinitions, registerNodeDefinitions, registerPluginCanvasNodes, shouldKeepAspectRatio, unregisterNodeDefinitions } from "./node-registry";
|
||||
import "./definitions";
|
||||
@@ -43,6 +43,8 @@ export type CanvasNodeDefinition = {
|
||||
generationMode?: (node: CanvasNodeData) => CanvasGenerationMode | null;
|
||||
/** 是否显示右侧输出连接点;缺省为 true,消费型终点节点可关闭。 */
|
||||
showOutputConnection?: boolean;
|
||||
/** 若设置,作为目标时只接受这一类上游节点。 */
|
||||
acceptsInputKind?: CanvasNodeInputKind;
|
||||
/**
|
||||
* 作为上游输入被参考素材容量校验计数时的类别;
|
||||
* 不设表示不参与计数(生成配置、背板)。与 resourceKind 不同,计数不看内容。
|
||||
@@ -65,6 +67,8 @@ export function canvasNodeDefinitionFromPlugin(pluginId: string, contribution: P
|
||||
defaultMetadata: { pluginId, pluginNodeId: contribution.id, pluginData: {}, content: "" },
|
||||
minSize: { width: Math.min(contribution.defaultSize.width, 220), height: Math.min(contribution.defaultSize.height, 160) },
|
||||
showInCreateMenu: true,
|
||||
showOutputConnection: contribution.showOutputConnection,
|
||||
acceptsInputKind: contribution.acceptsInputKind,
|
||||
plugin: { pluginId, renderer: contribution.renderer, schema: contribution.schema },
|
||||
};
|
||||
}
|
||||
@@ -96,3 +96,7 @@ export function getNodeGenerationMode(node: CanvasNodeData) {
|
||||
export function getNodeInputKind(type: CanvasNodeTypeId) {
|
||||
return definitions.get(type)?.inputKind;
|
||||
}
|
||||
|
||||
export function getNodeAcceptedInputKind(type: CanvasNodeTypeId) {
|
||||
return definitions.get(type)?.acceptsInputKind;
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
import { ScanSearch, Sparkles } from "lucide-react";
|
||||
import { createElement } from "react";
|
||||
|
||||
import type { FloatingDockEntry } from "@/components/ui/aceternity/floating-dock";
|
||||
import { ART_CRITIQUE_NODE_TYPE } from "@/lib/art-critique/contracts";
|
||||
import { listCreatableNodeDefinitions } from "@/lib/canvas/node-registry";
|
||||
|
||||
import type { AddNodeMenuCommand, AddNodeMenuContext, ToolCategory, ToolContext, ToolDefinition, ToolbarId, ToolbarPrefs } from "./tool-definition";
|
||||
|
||||
@@ -31,6 +36,28 @@ export function getAddNodeMenuCommands(): AddNodeMenuCommand[] {
|
||||
return [...addNodeMenuRegistry].sort((a, b) => a.defaultOrder - b.defaultOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将已注册的插件画布节点转换为添加菜单命令。
|
||||
* 这部分按需生成,避免远程/延迟加载的插件必须在工具定义模块初始化前完成注册。
|
||||
*/
|
||||
function getPluginNodeMenuCommands(): AddNodeMenuCommand[] {
|
||||
return listCreatableNodeDefinitions()
|
||||
.filter((definition) => Boolean(definition.plugin))
|
||||
.map((definition, index) => {
|
||||
const pluginId = definition.plugin!.pluginId;
|
||||
const FallbackIcon = definition.type === ART_CRITIQUE_NODE_TYPE ? ScanSearch : Sparkles;
|
||||
return {
|
||||
id: definition.type,
|
||||
label: definition.label,
|
||||
icon: definition.icon || createElement(FallbackIcon, { "aria-hidden": true }),
|
||||
section: "node",
|
||||
defaultOrder: 1000 + index,
|
||||
applicable: (ctx: AddNodeMenuContext) => !ctx.enabledPluginIds || ctx.enabledPluginIds.has(pluginId),
|
||||
run: (ctx: AddNodeMenuContext) => ctx.handlers.onAddExtensionNode(definition.type),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 默认偏好:全部工具按 defaultOrder 排列,全部可见 */
|
||||
export function defaultToolbarPrefs(toolbar: ToolbarId): ToolbarPrefs {
|
||||
const tools = getToolbarTools(toolbar);
|
||||
@@ -66,9 +93,9 @@ export function resolveToolbarTools(toolbar: ToolbarId, ctx: ToolContext, prefs:
|
||||
});
|
||||
}
|
||||
|
||||
/** 解析添加节点菜单命令——仅 applicable 过滤,不参与排序/显隐 */
|
||||
/** 解析添加节点菜单命令——合并插件节点后按 applicable 过滤并排序。 */
|
||||
export function resolveAddNodeMenuCommands(ctx: AddNodeMenuContext): AddNodeMenuCommand[] {
|
||||
return getAddNodeMenuCommands().filter((command) => !command.applicable || command.applicable(ctx));
|
||||
return [...getAddNodeMenuCommands(), ...getPluginNodeMenuCommands()].filter((command) => !command.applicable || command.applicable(ctx)).sort((a, b) => a.defaultOrder - b.defaultOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user