426 lines
16 KiB
JavaScript
426 lines
16 KiB
JavaScript
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;
|
|
}
|