Files
HadTavern/static/js/serialization.js

247 lines
9.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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');
}
// 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) Собираем ноды
let idx = 1;
for (const id in dfNodes) {
const df = dfNodes[id];
const genId = `n${idx++}`;
idMap[id] = genId;
const el = document.querySelector(`#node-${id}`);
// Берём источник правды из DOM.__data (куда жмём «Сохранить параметры») или из drawflow.data
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)));
nodes.push({
id: genId,
type: df.name,
pos_x: df.pos_x,
pos_y: df.pos_y,
config: datacopy,
in: {}
});
}
// 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 ?? '');
// conn.output может быть "output_1", "1" (строкой), либо числом 1
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;
}
if (!(sourceOutIdx >= 0)) sourceOutIdx = 0; // safety
const sourceNode = nodes.find(n => n.id === idMap[sourceDfId]);
if (!sourceNode) continue;
const sourceIo = NODE_IO[sourceNode.type] || { outputs: [] };
const sourceOutName = (sourceIo.outputs && sourceIo.outputs[sourceOutIdx] != null)
? sourceIo.outputs[sourceOutIdx]
: `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);
}
}
return { id: 'pipeline_editor', name: 'Edited Pipeline', nodes };
}
// pipeline JSON -> Drawflow
async function fromPipelineJSON(p) {
ensureDeps();
const editor = w.editor;
const NODE_IO = w.NODE_IO;
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,
};
})(window);