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

This commit is contained in:
2025-09-15 19:31:28 +03:00
parent 563663f9f1
commit 46d2fb8173
7 changed files with 723 additions and 375 deletions

View File

@@ -1003,26 +1003,10 @@ class PipelineExecutor:
def _safe_preview(self, obj: Any, max_bytes: int = 262_144) -> Any:
"""
Вернёт объект как есть, если его JSON-представление укладывается в лимит.
Иначе — мета-объект с пометкой о тримминге и SHA256 + текстовый превью.
Отменяем тримминг: сохраняем объект полностью.
Важно: это может сделать STORE очень большим для тяжёлых ответов.
"""
try:
s = json.dumps(obj, ensure_ascii=False)
except Exception:
try:
s = str(obj)
except Exception:
s = "<unrepresentable>"
b = s.encode("utf-8", errors="ignore")
if len(b) <= max_bytes:
return obj
sha = hashlib.sha256(b).hexdigest()
preview = b[:max_bytes].decode("utf-8", errors="ignore")
return {
"__truncated__": True,
"sha256": sha,
"preview": preview,
}
return obj
def _commit_snapshot(self, context: Dict[str, Any], values: Dict[str, Any], last_node_id: str) -> None:
"""
@@ -1050,8 +1034,7 @@ class PipelineExecutor:
txt = str(txt)
except Exception:
txt = ""
if len(txt) > 16_384:
txt = txt[:16_384]
# Без тримминга текста — сохраняем полный text
out_text[nid] = txt
out_raw[nid] = self._safe_preview(out, 262_144)
m = re.match(r"^n(\d+)$", str(nid))
@@ -1106,7 +1089,7 @@ class SetVarsNode(Node):
})
return norm
def _safe_eval_expr(self, expr: str) -> Any:
def _safe_eval_expr(self, expr: str, context: Optional[Dict[str, Any]] = None, out_map: Optional[Dict[str, Any]] = None) -> Any:
"""
Безопасная оценка выражений для SetVars.
@@ -1116,15 +1099,63 @@ class SetVarsNode(Node):
- Арифметика: + - * / // %, унарные +-
- Логика: and/or, сравнения (== != < <= > >=, цепочки)
- Безопасные функции: rand(), randint(a,b), choice(list)
- Новое: jp(value, path, join_sep="\\n") — JSONPathизвлечение из объекта/JSONстроки/макроса
jp_text(value, path, join_sep="\\n") — то же, но возвращает строку (склейка массива)
from_json(x) — распарсить JSONстроку (или макрос) в объект
Запрещено: имя/атрибуты/индексация/условные/импорты/прочие вызовы функций.
Примечание: функции jp/jp_text/from_json принимают как обычные значения,
так и строки с макросами ([[...]] или {{ ... }}). Макросы разворачиваются с помощью render_template_simple.
"""
import ast
import operator as op
import random
def _resolve_input(x: Any) -> Any:
# Если уже структура — вернуть как есть
if isinstance(x, (dict, list, bool, int, float)) or x is None:
return x
s = str(x)
# Попробуем развернуть макросы/шаблон, если есть контекст
try:
s2 = render_template_simple(s, context or {}, out_map or {})
except Exception:
s2 = s
# Попробуем распарсить JSON
try:
return json.loads(s2)
except Exception:
return s2
def _fn_from_json(x: Any) -> Any:
val = _resolve_input(x)
if isinstance(val, str):
try:
return json.loads(val)
except Exception:
return val
return val
def _fn_jp(value: Any, path: Any, join_sep: Any = "\n") -> Any:
src = _resolve_input(value)
p = str(path or "")
try:
vals = _json_path_extract(src, p)
return vals
except Exception:
return None
def _fn_jp_text(value: Any, path: Any, join_sep: Any = "\n") -> str:
vals = _fn_jp(value, path, join_sep)
try:
return _stringify_join(vals, str(join_sep or "\n"))
except Exception:
# на крайний случай — строковое представление
try:
return str(vals)
except Exception:
return ""
# 0) Попытаться распознать чистый JSONлитерал (включая true/false/null, объекты/массивы/числа/строки).
# Это не вмешивается в математику: для выражений вида "1+2" json.loads бросит исключение и мы пойдём в AST.
try:
s = str(expr).strip()
return json.loads(s)
@@ -1177,7 +1208,7 @@ class SetVarsNode(Node):
return False
left = right
return True
# Разрешённые вызовы: rand(), randint(a,b), choice(list)
# Разрешённые вызовы: rand(), randint(a,b), choice(list), jp(), jp_text(), from_json()
if isinstance(node, ast.Call):
# Никаких kwargs, *args
if node.keywords or isinstance(getattr(node, "starargs", None), ast.AST) or isinstance(getattr(node, "kwargs", None), ast.AST):
@@ -1208,6 +1239,24 @@ class SetVarsNode(Node):
if not seq:
raise ExecutionError("choice() on empty sequence")
return random.choice(seq)
if name == "jp":
# jp(value, path, join_sep? [ignored for non-text])
if len(node.args) < 2 or len(node.args) > 3:
raise ExecutionError("jp(value, path, [join_sep]) requires 2 or 3 args")
v = eval_node(node.args[0])
p = eval_node(node.args[1])
return _fn_jp(v, p, eval_node(node.args[2]) if len(node.args) == 3 else "\n")
if name == "jp_text":
if len(node.args) < 2 or len(node.args) > 3:
raise ExecutionError("jp_text(value, path, [join_sep]) requires 2 or 3 args")
v = eval_node(node.args[0])
p = eval_node(node.args[1])
return _fn_jp_text(v, p, eval_node(node.args[2]) if len(node.args) == 3 else "\n")
if name == "from_json":
if len(node.args) != 1:
raise ExecutionError("from_json(x) requires one argument")
v = eval_node(node.args[0])
return _fn_from_json(v)
raise ExecutionError(f"Function {name} is not allowed")
# Запрещаем всё остальное (Name/Attribute/Subscript/IfExp/Comprehensions и пр.)
raise ExecutionError("Expression not allowed")
@@ -1229,7 +1278,7 @@ class SetVarsNode(Node):
mode = v.get("mode", "string")
raw_val = v.get("value", "")
if mode == "expr":
resolved = self._safe_eval_expr(str(raw_val))
resolved = self._safe_eval_expr(str(raw_val), context, out_map)
else:
resolved = render_template_simple(str(raw_val or ""), context, out_map)
result[name] = resolved