This commit is contained in:
2025-09-11 17:27:15 +03:00
parent 3c77c3dc2e
commit 11a0535712
32 changed files with 4682 additions and 442 deletions

View File

@@ -40,6 +40,17 @@
const cancelBtn = document.getElementById('pm-cancel');
let editingId = null;
// Безопасное экранирование HTML для вставок в UI
function pmEscapeHtml(s) {
const str = String(s ?? '');
return str
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// Изменения блока применяются только по кнопке «Сохранить» внутри редактора блока.
// Drag&Drop через SortableJS (если доступен)
@@ -77,11 +88,13 @@
li.style.alignItems = 'center';
li.style.gap = '6px';
li.style.padding = '4px 0';
const nameDisp = pmEscapeHtml(b.name || ('Block ' + (i + 1)));
const roleDisp = pmEscapeHtml(b.role || 'user');
li.innerHTML = `
<span class="pm-handle" style="cursor:grab;">☰</span>
<input type="checkbox" class="pm-enabled" ${b.enabled !== false ? 'checked' : ''} title="enabled"/>
<span class="pm-name" style="flex:1">${(b.name || ('Block ' + (i + 1))).replace(/</g, '<')}</span>
<span class="pm-role" style="opacity:.8">${b.role || 'user'}</span>
<span class="pm-name" style="flex:1">${nameDisp}</span>
<span class="pm-role" style="opacity:.8">${roleDisp}</span>
<button class="pm-edit" title="Редактировать">✎</button>
<button class="pm-del" title="Удалить">🗑</button>
`;

View File

@@ -10,6 +10,39 @@
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();
@@ -22,20 +55,53 @@
const dfNodes = (data && data.drawflow && data.drawflow.Home && data.drawflow.Home.data) ? data.drawflow.Home.data : {};
// 1) Собираем ноды
let idx = 1;
// 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 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)));
try { datacopy._origId = idMap[id]; } catch (e) {}
nodes.push({
id: genId,
id: idMap[id],
type: df.name,
pos_x: df.pos_x,
pos_y: df.pos_y,
@@ -43,6 +109,7 @@
in: {}
});
}
try { console.debug('[AgentUISer.toPipelineJSON] idMap drawflowId->nX', idMap); } catch (e) {}
// 2) Восстанавливаем связи по входам (inputs)
// В Drawflow v0.0.55 inputs/outputs — это объекты вида input_1/output_1
@@ -56,15 +123,15 @@
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
// 1) Попробуем определить индекс выхода из conn.output
let sourceOutIdx = -1;
let m = outKey.match(/output_(\d+)/);
if (m) {
@@ -74,28 +141,72 @@
} 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}`;
// 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);
}
}
return { id: 'pipeline_editor', name: 'Edited Pipeline', nodes };
// 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
@@ -104,6 +215,19 @@
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
@@ -243,5 +367,7 @@
w.AgentUISer = {
toPipelineJSON,
fromPipelineJSON,
getPipelineMeta,
updatePipelineMeta,
};
})(window);