sync: UI animations, select styling, TLS verify flag via proxy second line, brand spacing

This commit is contained in:
2025-09-27 18:46:52 +03:00
parent 135c393eda
commit 2abfbb4b1a
52 changed files with 8029 additions and 1408 deletions

View File

@@ -21,10 +21,15 @@
// Готовим новые данные с глубокой копией blocks
const newData = { ...(n.data || {}), blocks: Array.isArray(d2.blocks) ? d2.blocks.map(b => ({ ...b })) : [] };
// 1) Обновляем внутреннее состояние Drawflow, чтобы export() возвращал актуальные данные
try { editor.updateNodeDataFromId(id, newData); } catch (e) {}
// 2) Обновляем DOM-отражение (источник правды для toPipelineJSON)
const el2 = document.querySelector(`#node-${id}`);
if (el2) el2.__data = JSON.parse(JSON.stringify(newData));
try {
if (w.AU && typeof w.AU.updateNodeDataAndDom === 'function') {
w.AU.updateNodeDataAndDom(editor, id, newData);
} else {
editor.updateNodeDataFromId(id, newData);
const el2 = document.querySelector(`#node-${id}`);
if (el2) el2.__data = JSON.parse(JSON.stringify(newData));
}
} catch (e) {}
} catch (e) {}
}
// Initial sync to attach blocks into __data for toPipelineJSON

View File

@@ -0,0 +1,158 @@
/* global window */
(function (w) {
'use strict';
// Centralized registry for provider-specific defaults (base_url, endpoint, headers, template)
// Exposes window.ProviderTemplates with:
// .register(name, { defaultConfig: () => ({ base_url, endpoint, headers, template }) })
// .defaults(provider)
// .ensureConfigs(nodeData)
// .getActiveProv(nodeData)
// .getActiveCfg(nodeData)
// .providers()
const PT = {};
const _registry = new Map();
function norm(p) {
return String(p == null ? 'openai' : p).toLowerCase().trim();
}
PT.register = function register(name, def) {
const key = norm(name);
if (!def || typeof def.defaultConfig !== 'function') {
throw new Error('ProviderTemplates.register: def.defaultConfig() required');
}
_registry.set(key, { defaultConfig: def.defaultConfig });
};
PT.providers = function providers() {
return Array.from(_registry.keys());
};
PT.defaults = function defaults(provider) {
const key = norm(provider);
const rec = _registry.get(key);
if (rec && typeof rec.defaultConfig === 'function') {
try { return rec.defaultConfig(); } catch (_) {}
}
return { base_url: '', endpoint: '', headers: `{}`, template: `{}` };
};
PT.ensureConfigs = function ensureConfigs(d) {
if (!d) return;
if (!d.provider) d.provider = 'openai';
if (!d.provider_configs || typeof d.provider_configs !== 'object') d.provider_configs = {};
for (const p of PT.providers()) {
if (!d.provider_configs[p]) d.provider_configs[p] = PT.defaults(p);
}
};
PT.getActiveProv = function getActiveProv(d) {
return norm(d && d.provider);
};
PT.getActiveCfg = function getActiveCfg(d) {
PT.ensureConfigs(d);
const p = PT.getActiveProv(d);
return d && d.provider_configs ? (d.provider_configs[p] || {}) : {};
};
// --- Built-in providers (default presets) ---
// Templates mirror original editor.html logic; use macros [[...]] and {{ ... }} as-is.
function T_OPENAI() { return `{
"model": "{{ model }}",
[[PROMPT]],
"temperature": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},
"top_p": {{ incoming.json.top_p|default(params.top_p|default(1)) }},
"max_tokens": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},
"max_completion_tokens": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},
"presence_penalty": {{ incoming.json.presence_penalty|default(0) }},
"frequency_penalty": {{ incoming.json.frequency_penalty|default(0) }},
"stop": {{ incoming.json.stop|default(params.stop|default([])) }},
"stream": {{ incoming.json.stream|default(false) }}
}`; }
function T_GEMINI() { return `{
"model": "{{ model }}",
[[PROMPT]],
"safetySettings": {{ incoming.json.safetySettings|default([]) }},
"generationConfig": {
"temperature": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},
"topP": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},
"maxOutputTokens": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},
"stopSequences": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},
"candidateCount": {{ incoming.json.generationConfig.candidateCount|default(1) }},
"thinkingConfig": {
"includeThoughts": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},
"thinkingBudget": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}
}
}
}`; }
function T_GEMINI_IMAGE() { return `{
"model": "{{ model }}",
[[PROMPT]]
}`; }
function T_CLAUDE() { return `{
"model": "{{ model }}",
[[PROMPT]],
"temperature": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},
"top_p": {{ incoming.json.top_p|default(params.top_p|default(1)) }},
"max_tokens": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},
"stop_sequences": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},
"stream": {{ incoming.json.stream|default(false) }},
"thinking": {
"type": "{{ incoming.json.thinking.type|default('disabled') }}",
"budget_tokens": {{ incoming.json.thinking.budget_tokens|default(0) }}
},
"anthropic_version": "{{ anthropic_version|default('2023-06-01') }}"
}`; }
// Register built-ins
PT.register('openai', {
defaultConfig: () => ({
base_url: 'https://api.openai.com',
endpoint: '/v1/chat/completions',
headers: `{"Authorization":"Bearer [[VAR:incoming.headers.authorization]]"}`,
template: T_OPENAI()
})
});
PT.register('gemini', {
defaultConfig: () => ({
base_url: 'https://generativelanguage.googleapis.com',
endpoint: '/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]',
headers: `{}`,
template: T_GEMINI()
})
});
PT.register('gemini_image', {
defaultConfig: () => ({
base_url: 'https://generativelanguage.googleapis.com',
endpoint: '/v1beta/models/{{ model }}:generateContent',
headers: `{"x-goog-api-key":"[[VAR:incoming.api_keys.key]]"}`,
template: T_GEMINI_IMAGE()
})
});
PT.register('claude', {
defaultConfig: () => ({
base_url: 'https://api.anthropic.com',
endpoint: '/v1/messages',
headers: `{"x-api-key":"[[VAR:incoming.headers.x-api-key]]","anthropic-version":"2023-06-01","anthropic-beta":"[[VAR:incoming.headers.anthropic-beta]]"}`,
template: T_CLAUDE()
})
});
try { console.debug('[ProviderTemplates] providers:', PT.providers()); } catch (_) {}
// Export globals and compatibility shims
try {
w.ProviderTemplates = PT;
// Back-compat shims so existing code can call global helpers
w.providerDefaults = PT.defaults;
w.ensureProviderConfigs = PT.ensureConfigs;
w.getActiveProv = PT.getActiveProv;
w.getActiveCfg = PT.getActiveCfg;
} catch (_) {}
})(window);

View File

@@ -12,7 +12,8 @@
// 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 = {
// DRY: единый источник дефолтов и нормализации meta
const MetaDefaults = Object.freeze({
id: 'pipeline_editor',
name: 'Edited Pipeline',
parallel_limit: 8,
@@ -20,19 +21,74 @@
loop_max_iters: 1000,
loop_time_budget_ms: 10000,
clear_var_store: true,
// New: default HTTP timeout for upstream requests (seconds)
http_timeout_sec: 60,
// New (v1): стратегия извлечения текста для [[OUTx]] (глобальная по умолчанию)
// auto | deep | openai | gemini | claude | jsonpath
text_extract_strategy: 'auto',
// Используется при стратегии jsonpath (dot-нотация, поддержка индексов: a.b.0.c)
text_extract_json_path: '',
// Разделитель при объединении массива результатов
text_join_sep: '\n',
// v2: коллекция пресетов извлечения текста, управляется в "Запуск"
// [{ id, name, strategy, json_path, join_sep }]
text_extract_presets: [],
};
});
let _pipelineMeta = { ...MetaDefaults };
// Нормализатор meta: приводит типы, поддерживает синонимы ключей, заполняет дефолты
function ensureMeta(p) {
const src = (p && typeof p === 'object') ? p : {};
const out = { ...MetaDefaults };
// helpers
const toInt = (v, def) => {
try {
const n = parseInt(v, 10);
return Number.isFinite(n) && n > 0 ? n : def;
} catch { return def; }
};
const toNum = (v, def) => {
try {
const n = parseFloat(v);
return !Number.isNaN(n) && n > 0 ? n : def;
} catch { return def; }
};
// базовые поля
try { out.id = String((src.id ?? out.id) || out.id); } catch {}
try { out.name = String((src.name ?? out.name) || out.name); } catch {}
out.parallel_limit = toInt(src.parallel_limit, out.parallel_limit);
out.loop_mode = String((src.loop_mode ?? out.loop_mode) || out.loop_mode);
out.loop_max_iters = toInt(src.loop_max_iters, out.loop_max_iters);
out.loop_time_budget_ms = toInt(src.loop_time_budget_ms, out.loop_time_budget_ms);
out.clear_var_store = (typeof src.clear_var_store === 'boolean') ? !!src.clear_var_store : out.clear_var_store;
out.http_timeout_sec = toNum(src.http_timeout_sec, out.http_timeout_sec);
out.text_extract_strategy = String((src.text_extract_strategy ?? out.text_extract_strategy) || out.text_extract_strategy);
out.text_extract_json_path = String((src.text_extract_json_path ?? out.text_extract_json_path) || out.text_extract_json_path);
// поддержка синонимов text_join_sep (регистр и вариации)
let joinSep = out.text_join_sep;
try {
for (const k of Object.keys(src)) {
if (String(k).toLowerCase() === 'text_join_sep') { joinSep = src[k]; break; }
}
} catch {}
out.text_join_sep = String((joinSep ?? src.text_join_sep ?? out.text_join_sep) || out.text_join_sep);
// коллекция пресетов
try {
const arr = Array.isArray(src.text_extract_presets) ? src.text_extract_presets : [];
out.text_extract_presets = arr
.filter(it => it && typeof it === 'object')
.map((it, idx) => ({
id: String((it.id ?? '') || ('p' + Date.now().toString(36) + Math.random().toString(36).slice(2) + idx)),
name: String(it.name ?? (it.json_path || 'Preset')),
strategy: String(it.strategy ?? 'auto'),
json_path: String(it.json_path ?? ''),
join_sep: String(it.join_sep ?? '\n'),
}));
} catch { out.text_extract_presets = []; }
return out;
}
function getPipelineMeta() {
return { ..._pipelineMeta };
@@ -40,48 +96,8 @@
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','http_timeout_sec',
'text_extract_strategy','text_extract_json_path','text_join_sep','text_join_sep','text_join_SEP',
// v2 presets collection
'text_extract_presets'
];
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 === 'http_timeout_sec') {
const fv = parseFloat(p[k]);
if (!Number.isNaN(fv) && fv > 0) _pipelineMeta[k] = fv;
} else if (k === 'clear_var_store') {
_pipelineMeta[k] = !!p[k];
} else {
// спец-обработка коллекции пресетов
if (k === 'text_extract_presets') {
try {
const arr = Array.isArray(p[k]) ? p[k] : [];
_pipelineMeta[k] = arr
.filter(it => it && typeof it === 'object')
.map(it => ({
id: String((it.id ?? '') || ('p' + Date.now().toString(36) + Math.random().toString(36).slice(2))),
name: String(it.name ?? 'Preset'),
strategy: String(it.strategy ?? 'auto'),
json_path: String(it.json_path ?? ''),
join_sep: String(it.join_sep ?? '\n'),
}));
} catch (_) {
_pipelineMeta[k] = [];
}
} else if (k.toLowerCase() === 'text_join_sep') {
// нормализация ключа join separator (допускаем разные написания)
_pipelineMeta['text_join_sep'] = String(p[k]);
} else {
_pipelineMeta[k] = String(p[k]);
}
}
}
}
// DRY: единая точка нормализации
_pipelineMeta = ensureMeta({ ..._pipelineMeta, ...p });
}
// Drawflow -> pipeline JSON
@@ -260,24 +276,10 @@
}
}
// 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),
http_timeout_sec: (typeof meta.http_timeout_sec === 'number' ? meta.http_timeout_sec : 60),
text_extract_strategy: (meta.text_extract_strategy || 'auto'),
text_extract_json_path: (meta.text_extract_json_path || ''),
text_join_sep: (meta.text_join_sep || '\n'),
// v2: persist presets
text_extract_presets: (Array.isArray(meta.text_extract_presets) ? meta.text_extract_presets : []),
nodes
};
// 3) Собираем итоговый pipeline JSON с метаданными (нормализованными)
const meta = ensureMeta(getPipelineMeta());
try { console.debug('[AgentUISer.toPipelineJSON] meta_keys', Object.keys(meta || {})); } catch (e) {}
return { ...meta, nodes };
}
// pipeline JSON -> Drawflow
@@ -285,25 +287,25 @@
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,
http_timeout_sec: (p && typeof p.http_timeout_sec === 'number') ? p.http_timeout_sec : 60,
text_extract_strategy: (p && typeof p.text_extract_strategy === 'string') ? p.text_extract_strategy : 'auto',
text_extract_json_path: (p && typeof p.text_extract_json_path === 'string') ? p.text_extract_json_path : '',
text_join_sep: (p && typeof p.text_join_sep === 'string') ? p.text_join_sep : '\n',
// v2: presets from pipeline.json
text_extract_presets: (p && Array.isArray(p.text_extract_presets)) ? p.text_extract_presets : [],
});
} catch (e) {}
// Сохраняем метаданные пайплайна для UI (сквозная нормализация)
try {
updatePipelineMeta(p || {});
// Диагностический лог состава meta для подтверждения DRY-рефакторинга
try {
const metaKeys = ["id","name","parallel_limit","loop_mode","loop_max_iters","loop_time_budget_ms","clear_var_store","http_timeout_sec","text_extract_strategy","text_extract_json_path","text_join_sep","text_extract_presets"];
const incomingKeys = metaKeys.filter(k => (p && Object.prototype.hasOwnProperty.call(p, k)));
const currentMeta = (typeof getPipelineMeta === 'function') ? getPipelineMeta() : {};
console.debug('[AgentUISer.fromPipelineJSON] meta_keys', {
incomingKeys,
resultKeys: Object.keys(currentMeta || {}),
metaPreview: {
id: currentMeta && currentMeta.id,
loop_mode: currentMeta && currentMeta.loop_mode,
http_timeout_sec: currentMeta && currentMeta.http_timeout_sec
}
});
} catch (_) {}
} catch (e) {}
editor.clear();
let x = 100; let y = 120; // Fallback

213
static/js/utils.js Normal file
View File

@@ -0,0 +1,213 @@
/* global window */
// AgentUI common UI utilities (DRY helpers shared by editor.html and pm-ui.js)
(function (w) {
'use strict';
const AU = {};
// HTML escaping for safe text/attribute insertion
AU.escapeHtml = function escapeHtml(s) {
const str = String(s ?? '');
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, "'");
};
// Attribute-safe escape (keeps quotes escaped; conservative)
AU.escAttr = function escAttr(v) {
const s = String(v ?? '');
return s
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, "'");
};
// Text-node escape (keeps quotes as-is for readability)
AU.escText = function escText(v) {
const s = String(v ?? '');
return s
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
};
// DRY helper: sync Drawflow node data + mirror into DOM.__data with deep copy
AU.updateNodeDataAndDom = function updateNodeDataAndDom(editor, id, data) {
try { editor && typeof editor.updateNodeDataFromId === 'function' && editor.updateNodeDataFromId(id, data); } catch (_) {}
try {
const el = document.querySelector('#node-' + id);
if (el) el.__data = JSON.parse(JSON.stringify(data));
} catch (_) {}
};
// Double rAF helper: waits for two animation frames; returns Promise or accepts callback
AU.nextRaf2 = function nextRaf2(cb) {
try {
if (typeof requestAnimationFrame === 'function') {
if (typeof cb === 'function') {
requestAnimationFrame(() => { requestAnimationFrame(() => { try { cb(); } catch (_) {} }); });
return;
}
return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
} else {
if (typeof cb === 'function') { setTimeout(() => { try { cb(); } catch (_) {} }, 32); return; }
return new Promise((resolve) => setTimeout(resolve, 32));
}
} catch (_) {
if (typeof cb === 'function') { try { cb(); } catch (__ ) {} }
return Promise.resolve();
}
};
// Heuristic: looks like long base64 payload
AU.isProbablyBase64 = function isProbablyBase64(s) {
try {
if (typeof s !== 'string') return false;
if (s.length < 64) return false;
return /^[A-Za-z0-9+/=\r\n]+$/.test(s);
} catch { return false; }
};
AU.trimBase64 = function trimBase64(s, maxLen = 180) {
try {
const str = String(s ?? '');
if (str.length > maxLen) {
return str.slice(0, maxLen) + `... (trimmed ${str.length - maxLen})`;
}
return str;
} catch { return String(s ?? ''); }
};
// Flatten JSON-like object into [path, stringValue] pairs
// Includes special handling for backend preview objects: { "__truncated__": true, "preview": "..." }
AU.flattenObject = function flattenObject(obj, prefix = '') {
const out = [];
if (obj == null) return out;
if (typeof obj !== 'object') {
out.push([prefix, String(obj)]);
return out;
}
try {
const entries = Object.entries(obj);
for (const [k, v] of entries) {
const p = prefix ? `${prefix}.${k}` : k;
if (v && typeof v === 'object' && !Array.isArray(v)) {
// Special preview shape from backend
if (Object.prototype.hasOwnProperty.call(v, '__truncated__') && Object.prototype.hasOwnProperty.call(v, 'preview')) {
out.push([p, String(v.preview ?? '')]);
continue;
}
out.push(...AU.flattenObject(v, p));
} else {
try {
const s = (typeof v === 'string') ? v : JSON.stringify(v, null, 0);
out.push([p, s]);
} catch {
out.push([p, String(v)]);
}
}
}
} catch {
// Fallback best-effort
try { out.push([prefix, JSON.stringify(obj)]); } catch { out.push([prefix, String(obj)]); }
}
return out;
};
// Format headers dictionary into text lines "Key: Value"
AU.fmtHeaders = function fmtHeaders(h) {
try {
const keys = Object.keys(h || {});
return keys.map(k => `${k}: ${String(h[k])}`).join('\n');
} catch { return ''; }
};
// Build HTTP request preview text
AU.buildReqText = function buildReqText(x) {
if (!x) return '';
const head = `${x.method || 'POST'} ${x.url || '/'} HTTP/1.1`;
const host = (() => {
try { const u = new URL(x.url); return `Host: ${u.host}`; } catch { return ''; }
})();
const hs = AU.fmtHeaders(x.headers || {});
const body = String(x.body_text || '').trim();
return [head, host, hs, '', body].filter(Boolean).join('\n');
};
// Build HTTP response preview text
AU.buildRespText = function buildRespText(x) {
if (!x) return '';
const head = `HTTP/1.1 ${x.status || 0}`;
const hs = AU.fmtHeaders(x.headers || {});
const body = String(x.body_text || '').trim();
return [head, hs, '', body].filter(Boolean).join('\n');
};
// Unified fetch helper with timeout and JSON handling
AU.apiFetch = async function apiFetch(url, opts) {
const t0 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
const o = opts || {};
const method = String(o.method || 'GET').toUpperCase();
const expectJson = (o.expectJson !== false); // default true
const headers = Object.assign({}, o.headers || {});
let body = o.body;
const timeoutMs = Number.isFinite(o.timeoutMs) ? o.timeoutMs : 15000;
const hasAbort = (typeof AbortController !== 'undefined');
const ctrl = hasAbort ? new AbortController() : null;
let to = null;
if (ctrl) {
try { to = setTimeout(() => { try { ctrl.abort(); } catch(_){} }, timeoutMs); } catch(_) {}
}
try {
if (expectJson) {
if (!headers['Accept'] && !headers['accept']) headers['Accept'] = 'application/json';
}
if (body != null) {
const isForm = (typeof FormData !== 'undefined' && body instanceof FormData);
const isBlob = (typeof Blob !== 'undefined' && body instanceof Blob);
if (typeof body === 'object' && !isForm && !isBlob) {
body = JSON.stringify(body);
if (!headers['Content-Type'] && !headers['content-type']) headers['Content-Type'] = 'application/json';
}
}
const res = await fetch(url, { method, headers, body, signal: ctrl ? ctrl.signal : undefined });
const ct = String(res.headers && res.headers.get ? (res.headers.get('Content-Type') || '') : '');
const isJsonCt = /application\/json/i.test(ct);
let data = null;
if (expectJson || isJsonCt) {
try { data = await res.json(); } catch (_) { data = null; }
} else {
try { data = await res.text(); } catch (_) { data = null; }
}
const t1 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
try { console.debug('[AU.apiFetch]', { method, url, status: res.status, ms: Math.round(t1 - t0) }); } catch(_) {}
if (!res.ok) {
const msg = (data && typeof data === 'object' && data.error) ? String(data.error) : `HTTP ${res.status}`;
const err = new Error(`apiFetch: ${msg}`);
err.status = res.status;
err.data = data;
err.url = url;
throw err;
}
return data;
} finally {
if (to) { try { clearTimeout(to); } catch(_) {} }
}
};
// Expose
try { w.AU = AU; } catch (_) {}
try { w.nextRaf2 = AU.nextRaf2; } catch (_) {}
})(window);