373 lines
16 KiB
JavaScript
373 lines
16 KiB
JavaScript
/* global window, document, requestAnimationFrame */
|
||
// AgentUI serialization helpers extracted from editor.html
|
||
// Exposes window.AgentUISer.{toPipelineJSON, fromPipelineJSON}
|
||
// Depends on globals defined by editor.html: editor, NODE_IO, addNode, applyNodeDefaults, status
|
||
(function (w) {
|
||
'use strict';
|
||
|
||
function ensureDeps() {
|
||
if (!w || !w.editor) throw new Error('AgentUISer: global editor is not available');
|
||
if (!w.NODE_IO) throw new Error('AgentUISer: global NODE_IO is not available');
|
||
}
|
||
|
||
// Top-level pipeline meta kept in memory and included into JSON on save.
|
||
// Allows UI to edit loop parameters without manual JSON edits.
|
||
let _pipelineMeta = {
|
||
id: 'pipeline_editor',
|
||
name: 'Edited Pipeline',
|
||
parallel_limit: 8,
|
||
loop_mode: 'dag',
|
||
loop_max_iters: 1000,
|
||
loop_time_budget_ms: 10000,
|
||
clear_var_store: true,
|
||
};
|
||
|
||
function getPipelineMeta() {
|
||
return { ..._pipelineMeta };
|
||
}
|
||
|
||
function updatePipelineMeta(p) {
|
||
if (!p || typeof p !== 'object') return;
|
||
const keys = ['id','name','parallel_limit','loop_mode','loop_max_iters','loop_time_budget_ms','clear_var_store'];
|
||
for (const k of keys) {
|
||
if (Object.prototype.hasOwnProperty.call(p, k) && p[k] !== undefined && p[k] !== null && (k === 'clear_var_store' ? true : p[k] !== '')) {
|
||
if (k === 'parallel_limit' || k === 'loop_max_iters' || k === 'loop_time_budget_ms') {
|
||
const v = parseInt(p[k], 10);
|
||
if (!Number.isNaN(v) && v > 0) _pipelineMeta[k] = v;
|
||
} else if (k === 'clear_var_store') {
|
||
_pipelineMeta[k] = !!p[k];
|
||
} else {
|
||
_pipelineMeta[k] = String(p[k]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Drawflow -> pipeline JSON
|
||
function toPipelineJSON() {
|
||
ensureDeps();
|
||
const editor = w.editor;
|
||
const NODE_IO = w.NODE_IO;
|
||
|
||
const data = editor.export();
|
||
const nodes = [];
|
||
const idMap = {}; // drawflow id -> generated id like n1, n2
|
||
|
||
const dfNodes = (data && data.drawflow && data.drawflow.Home && data.drawflow.Home.data) ? data.drawflow.Home.data : {};
|
||
|
||
// 1) Собираем ноды с устойчивыми id на основе _origId (если валиден/уникален)
|
||
const usedIds = new Set();
|
||
const wantIds = {}; // drawflow id -> желаемый/финальный nX
|
||
const isValidNid = (s) => typeof s === 'string' && /^n\d+$/i.test(s.trim());
|
||
|
||
// Первый проход: резервируем существующие валидные _origId
|
||
for (const id in dfNodes) {
|
||
const df = dfNodes[id];
|
||
const el = document.querySelector(`#node-${id}`);
|
||
const datacopySrc = el && el.__data ? el.__data : (df.data || {});
|
||
const tmp = typeof w.applyNodeDefaults === 'function'
|
||
? w.applyNodeDefaults(df.name, JSON.parse(JSON.stringify(datacopySrc)))
|
||
: (JSON.parse(JSON.stringify(datacopySrc)));
|
||
let desired = (tmp && typeof tmp._origId === 'string') ? String(tmp._origId).trim() : '';
|
||
if (isValidNid(desired) && !usedIds.has(desired)) {
|
||
wantIds[id] = desired;
|
||
usedIds.add(desired);
|
||
} else {
|
||
wantIds[id] = null; // назначим позже
|
||
}
|
||
}
|
||
// Поиск ближайшего свободного nX
|
||
function nextFreeId() {
|
||
let x = 1;
|
||
while (usedIds.has('n' + x)) x += 1;
|
||
return 'n' + x;
|
||
}
|
||
// Второй проход: назначаем конфликты/пустые
|
||
for (const id in dfNodes) {
|
||
if (!wantIds[id]) {
|
||
const nid = nextFreeId();
|
||
wantIds[id] = nid;
|
||
usedIds.add(nid);
|
||
}
|
||
idMap[id] = wantIds[id];
|
||
}
|
||
// Финальный проход: формируем массив нод, синхронизируя _origId
|
||
for (const id in dfNodes) {
|
||
const df = dfNodes[id];
|
||
const el = document.querySelector(`#node-${id}`);
|
||
const datacopySrc = el && el.__data ? el.__data : (df.data || {});
|
||
const datacopy = typeof w.applyNodeDefaults === 'function'
|
||
? w.applyNodeDefaults(df.name, JSON.parse(JSON.stringify(datacopySrc)))
|
||
: (JSON.parse(JSON.stringify(datacopySrc)));
|
||
try { datacopy._origId = idMap[id]; } catch (e) {}
|
||
nodes.push({
|
||
id: idMap[id],
|
||
type: df.name,
|
||
pos_x: df.pos_x,
|
||
pos_y: df.pos_y,
|
||
config: datacopy,
|
||
in: {}
|
||
});
|
||
}
|
||
try { console.debug('[AgentUISer.toPipelineJSON] idMap drawflowId->nX', idMap); } catch (e) {}
|
||
|
||
// 2) Восстанавливаем связи по входам (inputs)
|
||
// В Drawflow v0.0.55 inputs/outputs — это объекты вида input_1/output_1
|
||
for (const id in dfNodes) {
|
||
const df = dfNodes[id];
|
||
const targetNode = nodes.find(n => n.id === idMap[id]);
|
||
if (!targetNode) continue;
|
||
const io = NODE_IO[targetNode.type] || { inputs: [], outputs: [] };
|
||
|
||
for (let i = 0; i < io.inputs.length; i++) {
|
||
const inputKey = `input_${i + 1}`;
|
||
const input = df.inputs && df.inputs[inputKey];
|
||
if (!input || !Array.isArray(input.connections) || input.connections.length === 0) continue;
|
||
|
||
// Собираем все связи этого входа и сохраняем строку либо массив строк (для depends поддерживаем мульти-коннекты)
|
||
const refs = [];
|
||
for (const conn of (input.connections || [])) {
|
||
if (!conn) continue;
|
||
const sourceDfId = String(conn.node);
|
||
const outKey = String(conn.output ?? '');
|
||
|
||
// 1) Попробуем определить индекс выхода из conn.output
|
||
let sourceOutIdx = -1;
|
||
let m = outKey.match(/output_(\d+)/);
|
||
if (m) {
|
||
sourceOutIdx = parseInt(m[1], 10) - 1;
|
||
} else if (/^\d+$/.test(outKey)) {
|
||
sourceOutIdx = parseInt(outKey, 10) - 1;
|
||
} else if (typeof conn.output === 'number') {
|
||
sourceOutIdx = conn.output - 1;
|
||
}
|
||
|
||
const sourceNode = nodes.find(n => n.id === idMap[sourceDfId]);
|
||
if (!sourceNode) continue;
|
||
const sourceIo = NODE_IO[sourceNode.type] || { outputs: [] };
|
||
|
||
// 2) Fallback: если индекс не распознан или вне диапазона — проверим dfNodes[source].outputs
|
||
if (!(sourceOutIdx >= 0) || !(Array.isArray(sourceIo.outputs) && sourceIo.outputs[sourceOutIdx] != null)) {
|
||
try {
|
||
const srcDf = dfNodes[sourceDfId];
|
||
const outsObj = (srcDf && srcDf.outputs) ? srcDf.outputs : {};
|
||
let found = -1;
|
||
// Текущая целевая drawflow-нода — это id (внешняя переменная цикла по dfNodes)
|
||
const tgtDfId = id;
|
||
for (const k of Object.keys(outsObj || {})) {
|
||
const conns = (outsObj[k] && Array.isArray(outsObj[k].connections)) ? outsObj[k].connections : [];
|
||
if (conns.some(c => String(c && c.node) === String(tgtDfId))) {
|
||
const m2 = String(k).match(/output_(\d+)/);
|
||
if (m2) { found = parseInt(m2[1], 10) - 1; break; }
|
||
}
|
||
}
|
||
if (found >= 0) sourceOutIdx = found;
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
// 3) Ещё один safety: если до сих пор индекс невалидный — зажмём в границы
|
||
if (!(sourceOutIdx >= 0)) sourceOutIdx = 0;
|
||
if (Array.isArray(sourceIo.outputs) && sourceIo.outputs.length > 0) {
|
||
if (sourceOutIdx >= sourceIo.outputs.length) sourceOutIdx = sourceIo.outputs.length - 1;
|
||
}
|
||
|
||
// 4) Вычислим каноническое имя выхода по NODE_IO
|
||
let sourceOutName;
|
||
if (Array.isArray(sourceIo.outputs) && sourceIo.outputs[sourceOutIdx] != null) {
|
||
sourceOutName = sourceIo.outputs[sourceOutIdx];
|
||
} else {
|
||
// Fallback на технические имена (совместимость со старыми out0/out1)
|
||
sourceOutName = `out${sourceOutIdx}`;
|
||
}
|
||
|
||
refs.push(`${sourceNode.id}.${sourceOutName}`);
|
||
}
|
||
|
||
// Каноничное имя входа: по NODE_IO, иначе in{0-based}
|
||
const targetInName = (io.inputs && io.inputs[i] != null)
|
||
? io.inputs[i]
|
||
: `in${i}`;
|
||
|
||
if (!targetNode.in) targetNode.in = {};
|
||
targetNode.in[targetInName] = (refs.length <= 1 ? refs[0] : refs);
|
||
}
|
||
}
|
||
|
||
// 3) Собираем итоговый pipeline JSON с метаданными
|
||
const meta = getPipelineMeta();
|
||
return {
|
||
id: meta.id || 'pipeline_editor',
|
||
name: meta.name || 'Edited Pipeline',
|
||
parallel_limit: (typeof meta.parallel_limit === 'number' ? meta.parallel_limit : 8),
|
||
loop_mode: (meta.loop_mode || 'dag'),
|
||
loop_max_iters: (typeof meta.loop_max_iters === 'number' ? meta.loop_max_iters : 1000),
|
||
loop_time_budget_ms: (typeof meta.loop_time_budget_ms === 'number' ? meta.loop_time_budget_ms : 10000),
|
||
clear_var_store: (typeof meta.clear_var_store === 'boolean' ? meta.clear_var_store : true),
|
||
nodes
|
||
};
|
||
}
|
||
|
||
// pipeline JSON -> Drawflow
|
||
async function fromPipelineJSON(p) {
|
||
ensureDeps();
|
||
const editor = w.editor;
|
||
const NODE_IO = w.NODE_IO;
|
||
|
||
// Сохраняем метаданные пайплайна для UI
|
||
try {
|
||
updatePipelineMeta({
|
||
id: p && p.id ? p.id : 'pipeline_editor',
|
||
name: p && p.name ? p.name : 'Edited Pipeline',
|
||
parallel_limit: (p && typeof p.parallel_limit === 'number') ? p.parallel_limit : 8,
|
||
loop_mode: p && p.loop_mode ? p.loop_mode : 'dag',
|
||
loop_max_iters: (p && typeof p.loop_max_iters === 'number') ? p.loop_max_iters : 1000,
|
||
loop_time_budget_ms: (p && typeof p.loop_time_budget_ms === 'number') ? p.loop_time_budget_ms : 10000,
|
||
clear_var_store: (p && typeof p.clear_var_store === 'boolean') ? p.clear_var_store : true,
|
||
});
|
||
} catch (e) {}
|
||
|
||
editor.clear();
|
||
let x = 100; let y = 120; // Fallback
|
||
const idMap = {}; // pipeline id -> drawflow id
|
||
const logs = [];
|
||
const $ = (sel) => document.querySelector(sel);
|
||
|
||
const resolveOutIdx = (type, outName) => {
|
||
const outs = ((NODE_IO[type] && NODE_IO[type].outputs) || []);
|
||
let idx = outs.indexOf(outName);
|
||
if (idx < 0 && typeof outName === 'string') {
|
||
// поддержка: out-1, out_1, output_1, out1, out0
|
||
const s = String(outName);
|
||
let m = s.match(/^out(?:put)?[_-]?(\d+)$/);
|
||
if (m) {
|
||
const n = parseInt(m[1], 10);
|
||
idx = n > 0 ? n - 1 : 0;
|
||
} else {
|
||
m = s.match(/^out(\d+)$/); // совместимость со старым out0
|
||
if (m) idx = parseInt(m[1], 10) | 0;
|
||
}
|
||
}
|
||
return idx;
|
||
};
|
||
const resolveInIdx = (type, inName) => {
|
||
const ins = ((NODE_IO[type] && NODE_IO[type].inputs) || []);
|
||
let idx = ins.indexOf(inName);
|
||
if (idx < 0 && typeof inName === 'string') {
|
||
// поддержка: in-1, in_1, in1, in0
|
||
const s = String(inName);
|
||
let m = s.match(/^in[_-]?(\d+)$/);
|
||
if (m) {
|
||
const n = parseInt(m[1], 10);
|
||
idx = n > 0 ? n - 1 : 0;
|
||
} else {
|
||
m = s.match(/^in(\d+)$/); // совместимость со старым in0
|
||
if (m) idx = parseInt(m[1], 10) | 0;
|
||
}
|
||
}
|
||
return idx;
|
||
};
|
||
|
||
// Ожидание появления порта в DOM (устранение гонки рендера)
|
||
async function waitForPort(dfid, kind, idx, tries = 60, delay = 16) {
|
||
// Drawflow создаёт DOM-узел с id="node-${dfid}"
|
||
const sel = `#node-${dfid} .${kind}_${idx}`;
|
||
for (let i = 0; i < tries; i++) {
|
||
if ($(sel)) return true;
|
||
await new Promise(r => setTimeout(r, delay));
|
||
}
|
||
logs.push(`port missing: #${dfid} ${kind}_${idx}`);
|
||
return false;
|
||
}
|
||
// Повторные попытки соединить порты, пока DOM не готов
|
||
async function connectWithRetry(srcDfId, tgtDfId, outNum, inNum, tries = 120, delay = 25) {
|
||
const outClass = `output_${outNum}`;
|
||
const inClass = `input_${inNum}`;
|
||
for (let i = 0; i < tries; i++) {
|
||
const okOut = await waitForPort(srcDfId, 'output', outNum, 1, delay);
|
||
const okIn = await waitForPort(tgtDfId, 'input', inNum, 1, delay);
|
||
if (okOut && okIn) {
|
||
try {
|
||
editor.addConnection(srcDfId, tgtDfId, outClass, inClass);
|
||
return true;
|
||
} catch (e) {
|
||
// retry on next loop
|
||
}
|
||
}
|
||
await new Promise(r => setTimeout(r, delay));
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 1) Создаём ноды
|
||
for (const n of p.nodes) {
|
||
const pos = { x: n.pos_x || x, y: n.pos_y || y };
|
||
const data = { ...(n.config || {}), _origId: n.id };
|
||
const dfid = typeof w.addNode === 'function'
|
||
? w.addNode(n.type, pos, data)
|
||
: (function () { throw new Error('AgentUISer: addNode() is not defined'); })();
|
||
idMap[n.id] = dfid;
|
||
if (!n.pos_x) x += 260; // раскладываем по горизонтали, если нет сохраненной позиции
|
||
}
|
||
|
||
// 2) Дождёмся полного рендера DOM
|
||
await new Promise(r => setTimeout(r, 0));
|
||
if (typeof requestAnimationFrame === 'function') {
|
||
await new Promise(r => requestAnimationFrame(() => r()));
|
||
await new Promise(r => requestAnimationFrame(() => r())); // двойной rAF для надежности
|
||
} else {
|
||
await new Promise(r => setTimeout(r, 32));
|
||
}
|
||
// 3) Проставляем связи из in (поддержка строк и массивов ссылок)
|
||
for (const n of p.nodes) {
|
||
if (!n.in) continue;
|
||
const targetDfId = idMap[n.id];
|
||
const targetIo = NODE_IO[n.type] || { inputs: [] };
|
||
for (const [inName, ref] of Object.entries(n.in)) {
|
||
const refs = Array.isArray(ref) ? ref : [ref];
|
||
for (const oneRef of refs) {
|
||
if (!oneRef || typeof oneRef !== 'string' || !oneRef.includes('.')) continue;
|
||
const [srcId, outName] = oneRef.split('.');
|
||
const sourceDfId = idMap[srcId];
|
||
if (!sourceDfId) { logs.push(`skip: src ${srcId} not found`); continue; }
|
||
const srcType = p.nodes.find(nn => nn.id === srcId)?.type;
|
||
|
||
let outIdx = resolveOutIdx(srcType, outName);
|
||
let inIdx = resolveInIdx(n.type, inName);
|
||
|
||
// Fallback на первый порт, если неизвестные имена, но порт существует
|
||
if (outIdx < 0) outIdx = 0;
|
||
if (inIdx < 0) inIdx = 0;
|
||
|
||
const ok = await connectWithRetry(sourceDfId, targetDfId, outIdx + 1, inIdx + 1, 200, 25);
|
||
if (ok) {
|
||
logs.push(`connect: ${srcId}.${outName} (#${sourceDfId}.output_${outIdx + 1}) -> ${n.id}.${inName} (#${targetDfId}.input_${inIdx + 1})`);
|
||
} else {
|
||
logs.push(`skip connect (ports not ready after retries): ${srcId}.${outName} -> ${n.id}.${inName}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 4) Обновим линии и выведем лог
|
||
try {
|
||
Object.values(idMap).forEach((dfid) => {
|
||
if (editor.updateConnectionNodes) {
|
||
editor.updateConnectionNodes(`node-${dfid}`);
|
||
}
|
||
});
|
||
} catch {}
|
||
if (logs.length) {
|
||
try { (typeof w.status === 'function') && w.status('Загружено (links):\n' + logs.join('\n')); } catch {}
|
||
try { console.debug('[fromPipelineJSON]', logs); } catch {}
|
||
}
|
||
}
|
||
|
||
w.AgentUISer = {
|
||
toPipelineJSON,
|
||
fromPipelineJSON,
|
||
getPipelineMeta,
|
||
updatePipelineMeta,
|
||
};
|
||
})(window); |