sync: UI animations, select styling, TLS verify flag via proxy second line, brand spacing
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user