初始化

This commit is contained in:
2026-07-30 06:39:46 +08:00
commit 1a460f3f1d
23 changed files with 3025 additions and 0 deletions
+469
View File
@@ -0,0 +1,469 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { compileFlowPrompt, NODE_TYPES, normalizeChannel } from "./compiler.mjs";
import { nextAvailablePairedPosition, uniquePublisherNode } from "./node_actions.mjs";
import { parsePipelineConfig } from "./pipeline_config.mjs";
import { PIPELINE_NODE_MIN_WIDTH, setupPipelineEditor } from "./pipeline_editor.js";
const FLOW_TYPES = new Set(Object.values(NODE_TYPES));
const LEGACY_PUBLISHERS = new Set([NODE_TYPES.legacyStage, NODE_TYPES.legacyRoute]);
const LEGACY_CONSUMERS = new Set([NODE_TYPES.legacyStage, NODE_TYPES.legacyRoute]);
const COLORS = { ok: "#56b889", warning: "#d9a441", error: "#df6666", idle: "#7b8492" };
const STATUS_PRIORITY = { idle: 0, ok: 1, warning: 2, error: 3 };
const INACTIVE_MODES = new Set([2, 4]);
const INTERNAL_INPUTS = ["source", "pipeline_result", "selected_value"];
const INTERNAL_WIDGETS = [
"compile_error",
"pipeline_config",
"stage_name",
"selected_name",
"__stage_internal",
"__flow_generated",
];
const LABELS = {
channel: "结果名称",
value: "数据",
fallback: "找不到时使用",
input_channel: "起点结果",
output_channel: "最终发布为",
enabled: "启用阶段",
processed: "处理结果",
route: "选择方案",
option_1: "方案 1 结果",
option_2: "方案 2 结果",
option_3: "方案 3 结果",
condition: "条件",
on_true: "为真时",
on_false: "为假时",
};
const OUTPUT_LABELS = {
[NODE_TYPES.publish]: "数据",
[NODE_TYPES.get]: "数据",
[NODE_TYPES.pipeline]: "流程结果",
[NODE_TYPES.legacyStage]: "阶段结果",
[NODE_TYPES.legacyRoute]: "选择结果",
[NODE_TYPES.legacyIf]: "选择结果",
};
let refreshPending = false;
function ensureStyles() {
const id = "flow-branch-styles";
if (document.getElementById(id)) return;
const link = document.createElement("link");
link.id = id;
link.rel = "stylesheet";
link.href = new URL("./flow_branch.css", import.meta.url).href;
document.head.append(link);
}
function nodeType(node) {
return node?.comfyClass || node?.type || "";
}
function widget(node, name) {
return node.widgets?.find((item) => item.name === name);
}
function input(node, name) {
return node.inputs?.find((item) => item.name === name);
}
function isConnected(slot) {
return slot?.link !== null && slot?.link !== undefined;
}
function channelFor(node, direction) {
const type = nodeType(node);
const name = direction === "out"
? (type === NODE_TYPES.publish ? "channel" : "output_channel")
: (type === NODE_TYPES.get ? "channel" : "input_channel");
return normalizeChannel(widget(node, name)?.value);
}
function removeInputByName(node, name) {
const index = node.inputs?.findIndex((item) => item.name === name) ?? -1;
if (index >= 0) node.removeInput(index);
}
function hideWidget(item) {
if (!item) return;
item.hidden = true;
item.computeSize = () => [0, -4];
item.options = { ...(item.options || {}), serialize: true };
}
function hideInternalControls(node) {
for (const name of INTERNAL_INPUTS) removeInputByName(node, name);
for (const name of INTERNAL_WIDGETS) hideWidget(widget(node, name));
}
function applyChineseLabels(node) {
for (const item of node.widgets || []) {
if (LABELS[item.name]) item.label = LABELS[item.name];
}
for (const item of node.inputs || []) {
if (LABELS[item.name]) item.label = LABELS[item.name];
}
const outputLabel = OUTPUT_LABELS[nodeType(node)];
if (outputLabel && node.outputs?.[0]) node.outputs[0].label = outputLabel;
}
function setStatus(node, level, text, detail = text) {
node.__flowBranchStatus = { level, text, detail };
node.boxcolor = COLORS[level] || COLORS.idle;
}
function setHigherPriorityStatus(node, level, text, detail = text) {
const current = node.__flowBranchStatus;
if (!current || STATUS_PRIORITY[level] >= STATUS_PRIORITY[current.level]) {
setStatus(node, level, text, detail);
}
}
function addPublisher(publishers, channel, node, key = channel) {
if (!channel) return;
const matches = publishers.get(channel) || [];
matches.push({ node, key });
publishers.set(channel, matches);
}
function publisherHasValue(node) {
return nodeType(node) !== NODE_TYPES.publish || isConnected(input(node, "value"));
}
function registerPublishers(nodes, requireValue = true) {
const publishers = new Map();
for (const node of nodes) {
const type = nodeType(node);
if (type === NODE_TYPES.publish) {
const channel = channelFor(node, "out");
if (!requireValue || publisherHasValue(node)) addPublisher(publishers, channel, node);
} else if (LEGACY_PUBLISHERS.has(type)) {
addPublisher(publishers, channelFor(node, "out"), node);
} else if (type === NODE_TYPES.pipeline) {
const config = parsePipelineConfig(widget(node, "pipeline_config")?.value);
for (const stage of config.stages) addPublisher(publishers, normalizeChannel(stage.name), node, stage.id);
addPublisher(publishers, channelFor(node, "out"), node, "final");
}
}
return publishers;
}
function registerLivePublishers(nodes) {
return registerPublishers(nodes, true);
}
function graphPublishers(graph) {
return registerPublishers(graph?._nodes || [], false);
}
function createReaderNode(sourceNode, channel) {
const normalized = normalizeChannel(channel);
const graph = sourceNode?.graph || app.graph;
const reader = globalThis.LiteGraph?.createNode?.(NODE_TYPES.get);
if (!normalized || !graph || !reader) return null;
reader.pos = nextAvailablePairedPosition(sourceNode, reader, graph._nodes || []);
graph.add(reader);
const channelWidget = widget(reader, "channel");
if (channelWidget) {
channelWidget.value = normalized;
channelWidget.callback?.(normalized);
}
if (app.canvas?.graph === graph) app.canvas.selectNode?.(reader, false);
graph.setDirtyCanvas?.(true, true);
scheduleDiagnostics();
return reader;
}
function publisherForReader(node) {
const channel = channelFor(node, "in");
if (!channel) return null;
return uniquePublisherNode(graphPublishers(node.graph || app.graph).get(channel));
}
function jumpToNode(target) {
if (!target || app.canvas?.graph !== target.graph) return;
app.canvas.centerOnNode?.(target);
app.canvas.selectNode?.(target, false);
app.canvas.setDirty?.(true, true);
}
function pipelineReaderMenu(node) {
const config = parsePipelineConfig(widget(node, "pipeline_config")?.value);
const options = [];
for (const [index, stage] of config.stages.entries()) {
const channel = normalizeChannel(stage.name);
if (!channel) continue;
options.push({
content: `阶段 ${index + 1}${channel}`,
callback: () => createReaderNode(node, channel),
});
}
const finalChannel = channelFor(node, "out");
if (finalChannel) {
if (options.length) options.push(null);
options.push({
content: `最终结果:${finalChannel}`,
callback: () => createReaderNode(node, finalChannel),
});
}
return options;
}
function flowNodeMenuItems(node) {
const type = nodeType(node);
if (type === NODE_TYPES.publish) {
const channel = channelFor(node, "out");
return channel ? [{
content: `创建配对读取:${channel}`,
callback: () => createReaderNode(node, channel),
}] : [];
}
if (type === NODE_TYPES.pipeline) {
const options = pipelineReaderMenu(node);
return options.length ? [{
content: "创建结果读取节点",
has_submenu: true,
submenu: { options },
}] : [];
}
if (type === NODE_TYPES.get) {
const publisher = publisherForReader(node);
return publisher ? [{
content: "跳转到发送位置",
callback: () => jumpToNode(publisher),
}] : [];
}
return [];
}
function diagnosePublishers(nodes, publishers) {
for (const node of nodes) {
const type = nodeType(node);
if (type === NODE_TYPES.publish) {
const channel = channelFor(node, "out");
if (!channel) setStatus(node, "warning", "结果名称为空");
else if (!publisherHasValue(node)) setStatus(node, "warning", `未接数据 ${channel}`);
else {
const matches = publishers.get(channel) || [];
setStatus(
node,
matches.length > 1 ? "error" : "ok",
matches.length > 1 ? `名称冲突 ${channel}` : `发布 ${channel}`,
);
}
}
}
}
function diagnosePipeline(node, publishers) {
const config = parsePipelineConfig(widget(node, "pipeline_config")?.value);
const start = channelFor(node, "in");
const finalName = channelFor(node, "out");
const names = config.stages.map((stage) => normalizeChannel(stage.name));
const duplicate = names.find((name, index) => name && names.indexOf(name) !== index);
if (!start) setStatus(node, "error", "起点名称为空");
else if (!finalName) setStatus(node, "error", "最终名称为空");
else if (names.some((name) => !name)) setStatus(node, "error", "阶段名称为空");
else if (duplicate) setStatus(node, "error", `阶段重名 ${duplicate}`);
else {
const ownNames = new Set([...names, finalName]);
const conflict = [...ownNames].find((name) => (publishers.get(name) || []).length > 1);
const sources = (publishers.get(start) || []).filter((item) => item.node !== node);
if (conflict) setStatus(node, "error", `名称冲突 ${conflict}`);
else if (sources.length === 0) setStatus(node, "warning", `缺少起点 ${start}`);
else if (sources.length > 1) setStatus(node, "error", `起点冲突 ${start}`);
else if (config.stages.length === 0) setStatus(node, "warning", "尚未添加阶段");
else setStatus(node, "ok", `${config.stages.length} 个阶段`);
}
}
function diagnoseConsumer(node, publishers) {
const type = nodeType(node);
if (type !== NODE_TYPES.get && !LEGACY_CONSUMERS.has(type)) return;
const channel = channelFor(node, "in");
const matches = (publishers.get(channel) || []).filter((item) => item.node !== node);
if (!channel) setHigherPriorityStatus(node, "warning", "读取名称为空");
else if (matches.length === 1) setHigherPriorityStatus(node, "ok", `读取 ${channel}`);
else if (matches.length > 1) setHigherPriorityStatus(node, "error", `名称冲突 ${channel}`);
else setHigherPriorityStatus(node, "warning", `未找到 ${channel}`);
}
function refreshDiagnostics() {
refreshPending = false;
const allNodes = (app.graph?._nodes || []).filter((node) => FLOW_TYPES.has(nodeType(node)));
const nodes = allNodes.filter((node) => !INACTIVE_MODES.has(node.mode));
for (const node of allNodes) setStatus(node, "idle", "已停用");
for (const node of nodes) setStatus(node, "idle", "待检查");
const publishers = registerLivePublishers(nodes);
diagnosePublishers(nodes, publishers);
for (const node of nodes) {
if (nodeType(node) === NODE_TYPES.pipeline) diagnosePipeline(node, publishers);
diagnoseConsumer(node, publishers);
if (nodeType(node) === NODE_TYPES.legacyIf) setStatus(node, "idle", "旧版节点");
}
app.graph?.setDirtyCanvas(true, true);
}
function scheduleDiagnostics() {
if (refreshPending) return;
refreshPending = true;
requestAnimationFrame(refreshDiagnostics);
}
function wrapWidget(widgetItem, node) {
if (!widgetItem || widgetItem.__flowBranchWrapped) return;
widgetItem.__flowBranchWrapped = true;
const original = widgetItem.callback;
widgetItem.callback = function () {
const result = original?.apply(this, arguments);
if (nodeType(node) === NODE_TYPES.pipeline && widgetItem.name === "input_channel") {
node.__flowPipelineEditor?.render();
}
scheduleDiagnostics();
return result;
};
}
function fitStatusText(ctx, text, maxWidth) {
if (maxWidth <= 0) return "";
if (ctx.measureText(text).width <= maxWidth) return text;
let shortened = text;
while (shortened.length > 1 && ctx.measureText(`${shortened}...`).width > maxWidth) {
shortened = shortened.slice(0, -1);
}
return shortened.length ? `${shortened}...` : "";
}
function setupNode(node, reload = false) {
hideInternalControls(node);
applyChineseLabels(node);
const minWidth = nodeType(node) === NODE_TYPES.pipeline ? PIPELINE_NODE_MIN_WIDTH : 250;
node.setSize([Math.max(node.size[0], minWidth), node.size[1]]);
if (node.__flowBranchSetup) {
if (reload) node.__flowPipelineEditor?.reload();
scheduleDiagnostics();
return;
}
node.__flowBranchSetup = true;
for (const name of ["channel", "input_channel", "output_channel"]) {
wrapWidget(widget(node, name), node);
}
if (nodeType(node) === NODE_TYPES.pipeline) {
setupPipelineEditor(node, app, scheduleDiagnostics);
}
if (nodeType(node) === NODE_TYPES.get) {
const originalDoubleClick = node.onDblClick;
node.onDblClick = function () {
const publisher = publisherForReader(this);
if (publisher) {
jumpToNode(publisher);
return;
}
return originalDoubleClick?.apply(this, arguments);
};
}
const originalConnectionsChange = node.onConnectionsChange;
node.onConnectionsChange = function () {
const result = originalConnectionsChange?.apply(this, arguments);
this.__flowPipelineEditor?.refreshConnections();
scheduleDiagnostics();
return result;
};
const originalSerialize = node.onSerialize;
node.onSerialize = function () {
this.__flowPipelineEditor?.persist();
return originalSerialize?.apply(this, arguments);
};
const originalRemoved = node.onRemoved;
node.onRemoved = function () {
this.__flowPipelineEditor?.destroy();
const result = originalRemoved?.apply(this, arguments);
scheduleDiagnostics();
return result;
};
const originalModeChange = node.onModeChange;
node.onModeChange = function () {
const result = originalModeChange?.apply(this, arguments);
scheduleDiagnostics();
return result;
};
const originalDraw = node.onDrawForeground;
node.onDrawForeground = function (ctx) {
originalDraw?.apply(this, arguments);
this.__flowPipelineEditor?.updateCanvasPositions();
const status = this.__flowBranchStatus;
if (!status || this.flags?.collapsed) return;
ctx.save();
ctx.font = "11px sans-serif";
ctx.textAlign = "right";
ctx.textBaseline = "middle";
ctx.fillStyle = COLORS[status.level] || COLORS.idle;
const title = String(this.getTitle?.() || this.title || "");
const titleWidth = ctx.measureText(title).width;
const maxWidth = Math.max(0, this.size[0] - titleWidth - 48);
const text = fitStatusText(ctx, status.text, maxWidth);
if (text) ctx.fillText(text, this.size[0] - 10, -15);
ctx.restore();
};
scheduleDiagnostics();
}
function installPromptCompiler() {
if (api.__flowBranchCompilerInstalled) return;
api.__flowBranchCompilerInstalled = true;
const originalQueuePrompt = api.queuePrompt;
api.queuePrompt = async function () {
try {
const diagnostics = compileFlowPrompt(arguments[1]);
const warnings = diagnostics.filter((item) => item.level !== "ok");
if (warnings.length) console.warn("[FlowBranch]", warnings);
} catch (error) {
console.error("[FlowBranch] 编译流程失败", error);
}
return originalQueuePrompt.apply(this, arguments);
};
}
app.registerExtension({
name: "Comfy.FlowBranch",
init() {
ensureStyles();
},
setup() {
installPromptCompiler();
},
getNodeMenuItems(node) {
const items = flowNodeMenuItems(node);
return items.length ? [null, ...items] : [];
},
async beforeRegisterNodeDef(nodeType, nodeData) {
if (!FLOW_TYPES.has(nodeData.name)) return;
const originalCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
originalCreated?.apply(this, arguments);
setupNode(this);
};
if (nodeData.name === NODE_TYPES.pipeline) {
const originalConfigure = nodeType.prototype.configure;
nodeType.prototype.configure = function () {
const result = originalConfigure?.apply(this, arguments);
setupNode(this, true);
return result;
};
}
},
loadedGraphNode(node) {
if (FLOW_TYPES.has(nodeType(node))) setupNode(node, true);
},
});