初始化
This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
import { branchInputName, parsePipelineConfig } from "./pipeline_config.mjs";
|
||||
|
||||
export const NODE_TYPES = Object.freeze({
|
||||
publish: "FlowBranchPublish",
|
||||
get: "FlowBranchGet",
|
||||
pipeline: "FlowBranchPipeline",
|
||||
legacyStage: "FlowBranchStage",
|
||||
legacyRoute: "FlowBranchRoute",
|
||||
legacyIf: "FlowBranchIf",
|
||||
});
|
||||
|
||||
const LEGACY_PUBLISHERS = new Set([NODE_TYPES.legacyStage, NODE_TYPES.legacyRoute]);
|
||||
const LEGACY_CONSUMERS = new Set([NODE_TYPES.legacyStage, NODE_TYPES.legacyRoute]);
|
||||
const COMPILED_PROMPTS = new WeakMap();
|
||||
|
||||
export function normalizeChannel(value) {
|
||||
return String(value ?? "").trim();
|
||||
}
|
||||
|
||||
export function generatedStageNodeId(pipelineId, stageIndex) {
|
||||
return `__flowbranch_stage__${String(pipelineId)}__${stageIndex}`;
|
||||
}
|
||||
|
||||
function isLink(value) {
|
||||
return Array.isArray(value) && value.length === 2;
|
||||
}
|
||||
|
||||
function outputChannel(node) {
|
||||
if (node.class_type === NODE_TYPES.publish) return normalizeChannel(node.inputs?.channel);
|
||||
if (LEGACY_PUBLISHERS.has(node.class_type)) return normalizeChannel(node.inputs?.output_channel);
|
||||
if (node.class_type === NODE_TYPES.pipeline) return normalizeChannel(node.inputs?.output_channel);
|
||||
return "";
|
||||
}
|
||||
|
||||
function inputChannel(node) {
|
||||
if (node.class_type === NODE_TYPES.get) return normalizeChannel(node.inputs?.channel);
|
||||
if (LEGACY_CONSUMERS.has(node.class_type)) return normalizeChannel(node.inputs?.input_channel);
|
||||
if (node.class_type === NODE_TYPES.pipeline) return normalizeChannel(node.inputs?.input_channel);
|
||||
return "";
|
||||
}
|
||||
|
||||
function publisherHasValue(node) {
|
||||
return node.class_type !== NODE_TYPES.publish
|
||||
|| Object.prototype.hasOwnProperty.call(node.inputs || {}, "value");
|
||||
}
|
||||
|
||||
function addPublisher(publishers, channel, publisher) {
|
||||
if (!channel) return;
|
||||
const matches = publishers.get(channel) || [];
|
||||
matches.push(publisher);
|
||||
publishers.set(channel, matches);
|
||||
}
|
||||
|
||||
function channelConflictMessage(channel, matches) {
|
||||
const ids = matches.map((item) => `#${item.id}`).join("、");
|
||||
return `结果名称“${channel}”存在多个发布位置(${ids}),请改成唯一名称。`;
|
||||
}
|
||||
|
||||
function resolvePublisher(publishers, channel, consumerId, excludedPipelineId = null) {
|
||||
const matches = (publishers.get(channel) || []).filter((item) => {
|
||||
if (String(item.id) === String(consumerId)) return false;
|
||||
if (excludedPipelineId !== null && String(item.pipelineId) === String(excludedPipelineId)) return false;
|
||||
return true;
|
||||
});
|
||||
if (matches.length === 1) return { link: [matches[0].id, 0], matches };
|
||||
if (matches.length > 1) return { error: channelConflictMessage(channel, matches), matches };
|
||||
return { matches };
|
||||
}
|
||||
|
||||
function collectUpstreamFlowChannels(output, startLink) {
|
||||
const channels = new Set();
|
||||
const pending = isLink(startLink) ? [String(startLink[0])] : [];
|
||||
const visited = new Set();
|
||||
while (pending.length) {
|
||||
const nodeId = pending.pop();
|
||||
if (visited.has(nodeId)) continue;
|
||||
visited.add(nodeId);
|
||||
const node = output[nodeId];
|
||||
if (!node) continue;
|
||||
if (node.class_type === NODE_TYPES.get) channels.add(normalizeChannel(node.inputs?.channel));
|
||||
if (node.class_type === NODE_TYPES.publish) channels.add(normalizeChannel(node.inputs?.channel));
|
||||
if (node.class_type === NODE_TYPES.pipeline || LEGACY_PUBLISHERS.has(node.class_type)) {
|
||||
channels.add(outputChannel(node));
|
||||
}
|
||||
for (const value of Object.values(node.inputs || {})) {
|
||||
if (isLink(value)) pending.push(String(value[0]));
|
||||
}
|
||||
}
|
||||
channels.delete("");
|
||||
return channels;
|
||||
}
|
||||
|
||||
function branchLinkIsAvailable(output, publishers, link, expectedInput) {
|
||||
if (!isLink(link)) return false;
|
||||
const source = output[String(link[0])];
|
||||
if (!source) return false;
|
||||
if (source.class_type === NODE_TYPES.get) {
|
||||
const channel = inputChannel(source);
|
||||
if (channel === expectedInput) return false;
|
||||
if (isLink(source.inputs?.fallback)) return true;
|
||||
return Boolean(resolvePublisher(publishers, channel, String(link[0])).link);
|
||||
}
|
||||
if (source.class_type === NODE_TYPES.publish) return publisherHasValue(source);
|
||||
return true;
|
||||
}
|
||||
|
||||
function flowNodeIsUnavailable(node) {
|
||||
const inputs = node?.inputs || {};
|
||||
if (normalizeChannel(inputs.compile_error)) return false;
|
||||
if (node.class_type === NODE_TYPES.publish) return !publisherHasValue(node);
|
||||
if (node.class_type === NODE_TYPES.get) {
|
||||
return !isLink(inputs.source) && !isLink(inputs.fallback);
|
||||
}
|
||||
if (node.class_type === NODE_TYPES.pipeline) {
|
||||
return inputs.__stage_internal
|
||||
? !isLink(inputs.selected_value) && !isLink(inputs.source)
|
||||
: !isLink(inputs.pipeline_result) && !isLink(inputs.source);
|
||||
}
|
||||
if (node.class_type === NODE_TYPES.legacyStage) {
|
||||
if (inputs.enabled !== false && isLink(inputs.processed)) return false;
|
||||
return !isLink(inputs.source);
|
||||
}
|
||||
if (node.class_type === NODE_TYPES.legacyRoute) {
|
||||
const optionName = {
|
||||
"方案 1": "option_1",
|
||||
"方案 2": "option_2",
|
||||
"方案 3": "option_3",
|
||||
}[inputs.route];
|
||||
if (optionName && isLink(inputs[optionName])) return false;
|
||||
return !isLink(inputs.source);
|
||||
}
|
||||
if (node.class_type === NODE_TYPES.legacyIf) {
|
||||
return !isLink(inputs.on_true) && !isLink(inputs.on_false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function pruneUnavailableFlowLinks(output) {
|
||||
const unavailable = new Set();
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const [nodeId, node] of Object.entries(output)) {
|
||||
if (!unavailable.has(nodeId) && flowNodeIsUnavailable(node)) {
|
||||
unavailable.add(nodeId);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
for (const node of Object.values(output)) {
|
||||
for (const [name, value] of Object.entries(node.inputs || {})) {
|
||||
if (!isLink(value)) continue;
|
||||
const sourceId = String(value[0]);
|
||||
if (unavailable.has(sourceId)) {
|
||||
delete node.inputs[name];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return unavailable;
|
||||
}
|
||||
|
||||
function findLegacyCycleErrors(publishers) {
|
||||
const dependencyByOutput = new Map();
|
||||
for (const [channel, matches] of publishers) {
|
||||
if (matches.length !== 1) continue;
|
||||
const publisher = matches[0];
|
||||
if (!publisher.node || !LEGACY_PUBLISHERS.has(publisher.node.class_type)) continue;
|
||||
const dependency = inputChannel(publisher.node);
|
||||
if (dependency) dependencyByOutput.set(channel, { dependency, publisher });
|
||||
}
|
||||
|
||||
const errors = new Map();
|
||||
for (const start of dependencyByOutput.keys()) {
|
||||
const path = [];
|
||||
const positions = new Map();
|
||||
let channel = start;
|
||||
while (dependencyByOutput.has(channel)) {
|
||||
if (positions.has(channel)) {
|
||||
const cycle = path.slice(positions.get(channel));
|
||||
const message = `结果名称存在循环依赖:${[...cycle, channel].join(" → ")}`;
|
||||
for (const item of cycle) errors.set(dependencyByOutput.get(item).publisher.id, message);
|
||||
break;
|
||||
}
|
||||
positions.set(channel, path.length);
|
||||
path.push(channel);
|
||||
channel = dependencyByOutput.get(channel).dependency;
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function cleanCompilerInputs(node) {
|
||||
if (!node?.inputs) return;
|
||||
if (node.class_type === NODE_TYPES.get || LEGACY_CONSUMERS.has(node.class_type)) {
|
||||
delete node.inputs.source;
|
||||
node.inputs.compile_error = "";
|
||||
}
|
||||
if (node.class_type === NODE_TYPES.pipeline && !node.inputs.__stage_internal) {
|
||||
delete node.inputs.source;
|
||||
delete node.inputs.pipeline_result;
|
||||
node.inputs.compile_error = "";
|
||||
}
|
||||
}
|
||||
|
||||
function makeGeneratedStage(pipeline, stage, stageIndex, expectedInput, sourceLink) {
|
||||
const id = generatedStageNodeId(pipeline.id, stageIndex);
|
||||
const inputs = {
|
||||
input_channel: expectedInput,
|
||||
output_channel: stage.name,
|
||||
pipeline_config: "{\"version\":2,\"stages\":[]}",
|
||||
__stage_internal: true,
|
||||
__flow_generated: true,
|
||||
stage_name: stage.name,
|
||||
compile_error: "",
|
||||
};
|
||||
if (sourceLink) inputs.source = sourceLink;
|
||||
return {
|
||||
id,
|
||||
node: {
|
||||
class_type: NODE_TYPES.pipeline,
|
||||
inputs,
|
||||
_meta: { title: `${pipeline.node._meta?.title || "流程编排器"} / ${stage.name}` },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function registerPublishers(nodes, pipelines) {
|
||||
const publishers = new Map();
|
||||
for (const item of nodes) {
|
||||
if (item.node.class_type === NODE_TYPES.publish && publisherHasValue(item.node)) {
|
||||
addPublisher(publishers, outputChannel(item.node), { ...item, pipelineId: null });
|
||||
} else if (LEGACY_PUBLISHERS.has(item.node.class_type)) {
|
||||
addPublisher(publishers, outputChannel(item.node), { ...item, pipelineId: null });
|
||||
}
|
||||
}
|
||||
for (const pipeline of pipelines) {
|
||||
pipeline.config.stages.forEach((stage, stageIndex) => {
|
||||
addPublisher(publishers, normalizeChannel(stage.name), {
|
||||
id: generatedStageNodeId(pipeline.id, stageIndex),
|
||||
node: null,
|
||||
pipelineId: pipeline.id,
|
||||
stageIndex,
|
||||
});
|
||||
});
|
||||
const finalChannel = outputChannel(pipeline.node);
|
||||
addPublisher(publishers, finalChannel, {
|
||||
id: pipeline.id,
|
||||
node: pipeline.node,
|
||||
pipelineId: pipeline.id,
|
||||
stageIndex: null,
|
||||
});
|
||||
}
|
||||
return publishers;
|
||||
}
|
||||
|
||||
function compilePipeline(output, pipeline, publishers, diagnostics) {
|
||||
const inputs = pipeline.node.inputs || (pipeline.node.inputs = {});
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
const stageNames = new Set();
|
||||
const branchLinks = new Map();
|
||||
for (const [name, value] of Object.entries(inputs)) {
|
||||
if (name.startsWith("branch_") && isLink(value)) branchLinks.set(name, value);
|
||||
}
|
||||
|
||||
const startChannel = inputChannel(pipeline.node);
|
||||
const finalChannel = outputChannel(pipeline.node);
|
||||
if (!startChannel) errors.push("起点结果名称不能为空。");
|
||||
if (!finalChannel) errors.push("最终发布名称不能为空。");
|
||||
|
||||
for (const stage of pipeline.config.stages) {
|
||||
const stageName = normalizeChannel(stage.name);
|
||||
if (!stageName) errors.push("阶段结果名称不能为空。");
|
||||
else if (stageNames.has(stageName)) errors.push(`阶段结果名称“${stageName}”重复。`);
|
||||
stageNames.add(stageName);
|
||||
}
|
||||
|
||||
for (const channel of [...stageNames, finalChannel].filter(Boolean)) {
|
||||
const matches = publishers.get(channel) || [];
|
||||
if (matches.length > 1) errors.push(channelConflictMessage(channel, matches));
|
||||
}
|
||||
|
||||
const start = resolvePublisher(publishers, startChannel, pipeline.id, pipeline.id);
|
||||
if (start.error) errors.push(start.error);
|
||||
else if (!start.link && startChannel) {
|
||||
warnings.push(`起点结果“${startChannel}”本次没有可用数据,流程将保持为空。`);
|
||||
}
|
||||
|
||||
let previousLink = start.link;
|
||||
let previousAvailable = Boolean(start.link);
|
||||
let expectedInput = startChannel;
|
||||
for (const [stageIndex, stage] of pipeline.config.stages.entries()) {
|
||||
const generated = makeGeneratedStage(
|
||||
pipeline,
|
||||
stage,
|
||||
stageIndex,
|
||||
expectedInput,
|
||||
previousAvailable ? previousLink : null,
|
||||
);
|
||||
const stageErrors = [...errors];
|
||||
if (stage.enabled && previousAvailable) {
|
||||
let selectedBranch = null;
|
||||
let selectedLink = null;
|
||||
if (stage.autoSelect) {
|
||||
const available = stage.branches.flatMap((branch) => {
|
||||
const link = branchLinks.get(branchInputName(branch.id));
|
||||
return branchLinkIsAvailable(output, publishers, link, expectedInput)
|
||||
? [{ branch, link }]
|
||||
: [];
|
||||
});
|
||||
if (available.length) {
|
||||
({ branch: selectedBranch, link: selectedLink } = available[0]);
|
||||
if (available.length > 1) {
|
||||
warnings.push(
|
||||
`阶段“${stage.name}”检测到多个可用方案,已按从上到下选择“${selectedBranch.name}”。`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warnings.push(`阶段“${stage.name}”没有可用方案,将直接沿用上一阶段。`);
|
||||
}
|
||||
} else if (stage.selected) {
|
||||
selectedBranch = stage.branches.find((item) => item.id === stage.selected) || null;
|
||||
selectedLink = selectedBranch ? branchLinks.get(branchInputName(selectedBranch.id)) : null;
|
||||
}
|
||||
|
||||
if (selectedBranch && selectedLink) {
|
||||
const upstreamChannels = collectUpstreamFlowChannels(output, selectedLink);
|
||||
if (!upstreamChannels.has(expectedInput)) {
|
||||
stageErrors.push(
|
||||
`阶段“${stage.name}”的方案“${selectedBranch.name}”必须读取上一阶段“${expectedInput}”,`
|
||||
+ "否则会跳过前面的处理。",
|
||||
);
|
||||
} else {
|
||||
generated.node.inputs.selected_value = selectedLink;
|
||||
generated.node.inputs.selected_name = selectedBranch.name;
|
||||
}
|
||||
} else if (!stage.autoSelect && stage.selected && !selectedLink) {
|
||||
warnings.push(`阶段“${stage.name}”选中的方案未连接,将直接沿用上一阶段。`);
|
||||
}
|
||||
}
|
||||
if (stageErrors.length) generated.node.inputs.compile_error = [...new Set(stageErrors)].join(" ");
|
||||
output[generated.id] = generated.node;
|
||||
previousLink = [generated.id, 0];
|
||||
expectedInput = normalizeChannel(stage.name);
|
||||
}
|
||||
|
||||
for (const name of [...Object.keys(inputs)]) {
|
||||
if (name.startsWith("branch_")) delete inputs[name];
|
||||
}
|
||||
|
||||
if (previousAvailable && previousLink) inputs.pipeline_result = previousLink;
|
||||
if (errors.length) inputs.compile_error = [...new Set(errors)].join(" ");
|
||||
|
||||
if (errors.length) {
|
||||
diagnostics.push({ nodeId: pipeline.id, level: "error", message: inputs.compile_error });
|
||||
} else if (warnings.length || pipeline.config.stages.length === 0) {
|
||||
diagnostics.push({
|
||||
nodeId: pipeline.id,
|
||||
level: "warning",
|
||||
message: warnings[0] || "尚未添加阶段,将直接传递起点结果。",
|
||||
});
|
||||
} else {
|
||||
diagnostics.push({
|
||||
nodeId: pipeline.id,
|
||||
level: "ok",
|
||||
message: `已编排 ${pipeline.config.stages.length} 个阶段。`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function compileNamedConsumers(nodes, publishers, diagnostics) {
|
||||
const cycleErrors = findLegacyCycleErrors(publishers);
|
||||
for (const consumer of nodes) {
|
||||
if (consumer.node.class_type !== NODE_TYPES.get && !LEGACY_CONSUMERS.has(consumer.node.class_type)) {
|
||||
continue;
|
||||
}
|
||||
const inputs = consumer.node.inputs || (consumer.node.inputs = {});
|
||||
if (cycleErrors.has(consumer.id)) {
|
||||
inputs.compile_error = cycleErrors.get(consumer.id);
|
||||
diagnostics.push({ nodeId: consumer.id, level: "error", message: inputs.compile_error });
|
||||
continue;
|
||||
}
|
||||
const channel = inputChannel(consumer.node);
|
||||
if (!channel) {
|
||||
diagnostics.push({ nodeId: consumer.id, level: "warning", message: "读取结果名称为空。" });
|
||||
continue;
|
||||
}
|
||||
const resolved = resolvePublisher(publishers, channel, consumer.id);
|
||||
if (resolved.link) {
|
||||
inputs.source = resolved.link;
|
||||
diagnostics.push({ nodeId: consumer.id, level: "ok", message: `已读取“${channel}”。` });
|
||||
} else if (resolved.error) {
|
||||
inputs.compile_error = resolved.error;
|
||||
diagnostics.push({ nodeId: consumer.id, level: "error", message: resolved.error });
|
||||
} else {
|
||||
diagnostics.push({ nodeId: consumer.id, level: "warning", message: `找不到结果“${channel}”,运行时尝试回退。` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function inspectFlowPrompt(output) {
|
||||
const nodes = Object.entries(output || {}).map(([id, node]) => ({ id: String(id), node }));
|
||||
const pipelines = nodes
|
||||
.filter((item) => item.node.class_type === NODE_TYPES.pipeline && !item.node.inputs?.__stage_internal)
|
||||
.map((item) => ({ ...item, config: parsePipelineConfig(item.node.inputs?.pipeline_config) }));
|
||||
return { nodes, pipelines };
|
||||
}
|
||||
|
||||
export function compileFlowPrompt(prompt) {
|
||||
if (!prompt || typeof prompt !== "object") return [];
|
||||
if (COMPILED_PROMPTS.has(prompt)) return COMPILED_PROMPTS.get(prompt);
|
||||
const output = prompt.output || (prompt.output = {});
|
||||
if (Object.values(output).some((node) => node?.inputs?.__flow_generated)) return [];
|
||||
|
||||
const { nodes, pipelines } = inspectFlowPrompt(output);
|
||||
for (const item of nodes) cleanCompilerInputs(item.node);
|
||||
const publishers = registerPublishers(nodes, pipelines);
|
||||
const diagnostics = [];
|
||||
compileNamedConsumers(nodes, publishers, diagnostics);
|
||||
for (const pipeline of pipelines) compilePipeline(output, pipeline, publishers, diagnostics);
|
||||
pruneUnavailableFlowLinks(output);
|
||||
COMPILED_PROMPTS.set(prompt, diagnostics);
|
||||
return diagnostics;
|
||||
}
|
||||
Reference in New Issue
Block a user