AI-автоматизация "в браузере": Создаем независимых веб-агентов в среде ZennoPoster

SAT

Client
Регистрация
24.12.2024
Сообщения
56
Благодарностей
117
Баллы
33
AI-АВТОМАТИЗАЦИЯ «В БРАУЗЕРЕ»

Создаём независимых веб-агентов в среде ZennoPoster

Полный разбор архитектуры: DOM-парсинг · LCM · State Management · Self-Verification
140538

Введение: когда API закрыт, а задача открыта
Представьте ситуацию: вам нужно автоматизировать работу с мощным ИИ — GPT-4o, Claude 3 Opus, Gemini 1.5 Pro. Но API либо слишком дорогой, либо закрыт на вашем тарифе, либо его попросту нет для нужного сервиса. Классический путь заблокирован. Мы идём другим путём.
Этот кейс — про создание полноценного ИИ-агента, который работает с веб-версиями нейросетей так же, как это делает живой человек: читает страницу, отправляет промпты, парсит ответы, кликает по целевому сайту — и всё это без единого API-ключа, прямо внутри ZennoPoster.
Но главная ценность здесь — не трюк с обходом API. Главная ценность — архитектура с управлением состоянием (State Management), которая делает бота устойчивым, самопроверяющимся и адаптивным. Это тот уровень, когда бот не просто кликает по шаблону, а думает о результате каждого своего действия.


Проблема, которую мы решаем
Три стены, в которые упираются классические подходы
Стена 1: API-лимиты и деньги.
Топовые LLM через API стоят ощутимо. Проект на 100 000+ запросов в месяц легко выходит за $500. Веб-версии тех же моделей — фиксированная подписка $20/месяц или вообще бесплатны.
Стена 2: Хрупкость классических ботов. Традиционный ZP-бот завязан на XPath, CSS-селекторы и точные координаты. Сайт обновил вёрстку — всё сломалось. Наш агент каждый раз анализирует страницу заново: он не ломается от редизайна.
Стена 3: Галлюцинации ИИ при длинных цепочках. Если просить ИИ спланировать 15 шагов вперёд — он начинает фантазировать. Наша архитектура решает это через атомарное планирование: один шаг = один запрос к ИИ.

Наше решение: браузер как нативный интерфейс

Концепция в одной фразе
Вместо того чтобы стучаться в API, мы превращаем ZennoPoster в руки и глаза AI-агента.
Агент открывает ChatGPT/Gemini/Claude в браузере так же, как это делает человек,
отправляет туда промпты и управляет целевым сайтом на основе ответов ИИ.

Это открывает уникальные возможности:
  • Работа с любым веб-ИИ без API: ChatGPT Plus, Claude Pro, Gemini Advanced, Grok — неважно
  • Мультиокончность: в одной вкладке ChatGPT, в другой — целевой сайт, в третьей — ещё один ИИ
  • Нулевая зависимость от вёрстки: ИИ перечитывает DOM при каждом шаге
  • Self-verification: агент сам проверяет, сработало ли его действие
Архитектура: анатомия агента под капотом
Система состоит из шести взаимосвязанных модулей, работающих по непрерывному циклу. Прежде чем разбирать каждый модуль отдельно — посмотрим на общую картину.

Главный цикл агента:

ЭтапОписание
1. ПланированиеPython-планировщик разбивает цель на атомарные шаги, каждый записывается в task.txt
2. DOM-сканированиеAI DOM Mapper собирает все элементы страницы, Препроцессор фильтрует «мусор»
3. Формирование промптаSnippet A собирает DOM + задачу + контекст → создаёт точный промпт для ИИ
4. Парсинг ответа ИИSnippet B извлекает action_chain.json из ответа нейросети, нормализует команды
5. Выполнение действияSnippet 2 кликает/вводит текст, снимает скриншот ДО и ПОСЛЕ, фиксирует DOM-diff
6. ВерификацияSnippets C→D→3 отправляют ИИ вопрос "всё ли получилось?" и реагируют на ответ
После успешной верификации — шаг засчитан, планировщик активирует следующий. При RETRY/ERROR — агент перестраивает план.

Модуль 1: Task Chain Planner (Python)
Всё начинается с задачи. Не с клика и не с промпта — именно с высокоуровневой цели, которую нужно разложить на атомарные действия.

Зачем нужен отдельный планировщик?
Это принципиальное архитектурное решение. ИИ не умеет надёжно планировать 10-15 шагов вперёд: он галлюцинирует, теряет контекст, генерирует нереалистичные последовательности. Вместо этого мы делаем декомпозицию: один файл = одно атомарное действие. Каждый шаг — это точная инструкция для Logic Control Module.

Как работает планировщик
Планировщик запускается один раз перед стартом задачи. Он берёт вашу цель (например, "Зарегистрироваться на site.com и заполнить профиль") и через локальную LLM (Ollama/llama3) разбивает её на шаги:

# Пример: цель → цепочка атомарных шагов

# Запуск планировщика:
python task_chain_planner.py "Register on example.com and fill profile"

# Результат (task_chain/task_plan.json):


JSON:
{
  "goal": "Register on example.com and fill profile",
  "total_steps": 4,
  "steps": [
    {"step_num": 1, "prompt": "Navigate to https://example.com/register"},
    {"step_num": 2, "prompt": "Click the Sign Up button"},
    {"step_num": 3, "after_nav": true,
      "prompt": "Type your email into the Email field"},
    {"step_num": 4, "after_nav": true,
      "prompt": "Click the Create Account button"}
  ]
}
Флаг after_nav: true — особенность системы. Если шаг появляется только после навигации (например, поле ввода на новой странице), планировщик помечает это. Тогда к промпту автоматически добавляется контекст: "The browser is already on page: URL. Find and click…" — это не даёт ИИ придумывать навигацию туда, куда уже пришли.

Режимы работы планировщика

  • python task_chain_planner.py "цель" — создать новый план из цели
  • python task_chain_planner.py --next — активировать следующий шаг (вызывается ZP после верификации)
  • python task_chain_planner.py --activate 3 — принудительно активировать конкретный шаг
  • python task_chain_planner.py --show — показать текущий план

Ключевые файлы, которые создаёт планировщик
task_chain/task_plan.json — полный план в JSON
task_chain/task_step_01.txt ... task_step_NN.txt — отдельный файл для каждого шага
goal.txt — активный шаг (его читает ZennoPoster в следующем цикле)
context_store.json — накопленный контекст из предыдущих сессий

Модуль 2: «Глаза» агента — AI DOM Mapper + Preprocessor
Чтобы ИИ понял, с чем работает, ему нужно "увидеть" страницу. Передавать сырой HTML нельзя — современная страница весит 500-2000 КБ HTML и содержит тысячи узлов. Это убьёт контекстное окно и стоит дорого.

Двухэтапная обработка DOM
Этап 1 — AI DOM Mapper.
JavaScript-код, запускаемый через EvaluateScript, обходит ВСЁ дерево элементов: обычный DOM, Shadow DOM внутри веб-компонентов, iframe'ы. Каждый элемент получает уникальный ID, координаты центра (cx/cy), размеры, тег, текст, ARIA-атрибуты, тип интерактивности.
Этап 2 — AI DOM Preprocessor. Это "мозг" препроцессора. Он берёт собранное дерево и безжалостно вырезает всё лишнее: невидимые элементы (w=0, h=0), disabled-поля, элементы без реальных координат. Остаются только интерактивные узлы с понятными метками.

// Логика фильтрации в Preprocessor (упрощённо):


C#:
raw.elements.forEach(function(e) {
  if (!e.visible || !e.w || !e.h) return;  // невидимые — долой
  if (e.interactive) { flt.push(e); return; }  // интерактивные — берём
  if (e.isImage && e.w >= 16 && e.h >= 16) { flt.push(e); return; }
  if (e.isNav && e.w > 50 && e.h > 50) { flt.push(e); return; }
  if (tag === "h1" || tag === "h2") { flt.push(e); return; }
});
// Результат: из 1200 элементов остаётся ~40-80 ключевых

Инженерная хитрость: передача данных через DOM-атрибуты
ZennoPoster имеет ограничение: EvaluateScript не возвращает значения напрямую. Мы решили это элегантно: JavaScript записывает результат в скрытый div, разбивая JSON на чанки по 50 000 символов в атрибуты span-элементов. C# читает эти атрибуты по одному и склеивает итоговый JSON.

// JS пишет результат в DOM (чанки по 50 000 символов):


C#:
var rd = document.createElement("div");

rd.id = "zp_r"; rd.style = "display:none";
rd.setAttribute("data-n", String(rChunks));  // кол-во чанков

for (var j = 0; j < rChunks; j++) {
  var rs = document.createElement("span");
  rs.setAttribute("data-i", String(j));
  rs.setAttribute("data-v", out.substring(j*50000, (j+1)*50000));
  rd.appendChild(rs);
}
// C# читает чанки и собирает JSON:

C#:
for (int i = 0; i < nChunks; i++) {
  var chunkSpan = instance.ActiveTab.FindElementByXPath(
    "//div[@id='zp_r']/span[@data-i='" + i + "']", 0);
  sb.Append(chunkSpan.GetAttribute("data-v"));
}
Результат: сжатие в 10-30 раз. Страница с 1200 элементов превращается в компактный JSON на 40-80 позиций. ИИ отвечает мгновенно и точно.


Модуль 3: Snippet A — Формирование промпта для LCM
Хороший промпт — половина успеха. Snippet A формирует структурированный запрос к ИИ, объединяя три источника данных: задачу, состояние DOM и накопленный контекст.

Структура промпта

JSON:
=== SYSTEM PROMPT ===
You are a precise web automation planner for ZennoPoster.
Your ENTIRE response must be a single valid JSON object.
Start with { and end with }. No markdown. No text before/after.

=== USER PROMPT ===
TASK:
<содержимое task.txt>

SAVED CONTEXT (state from previous runs):
  logged_in_user: user@example.com  (saved at 2024-01-15 14:30)

CURRENT PAGE:
URL: https://example.com/dashboard
Title: Dashboard — Example

AVAILABLE DOM UI ELEMENTS (x/y are center coordinates):
[{"tag":"button","label":"New Project","x":145,"y":88,"w":120,"h":36},
{"tag":"input","label":"Search","x":440,"y":52,"w":280,"h":36},
{"tag":"a","label":"Settings","x":1180,"y":52,"w":80,"h":36}]

CRITICAL: Plan ONLY steps needed. Output ONLY valid JSON.
Предобработка DOM внутри Snippet A
Snippet A не просто вставляет DOM как есть. Он повторяет логику Python LCM: фильтрует только interactive элементы, нормализует координаты (приоритет cx/cy как центр элемента над x/y как угол), убирает элементы без меток и с CSS-мусором в текстах.

// Нормализация координат (C#):


C#:
int finalX = cx > 0 ? cx : x;  // центр важнее угла
int finalY = cy > 0 ? cy : y;
if (finalX == 0 && finalY == 0) continue;  // без координат — бесполезен
// Сборка метки: приоритет label > text > aria-label > placeholder > id
foreach (var lk in new[] {"label","text","aria-label","placeholder","title"}) {
  string lv = e[lk]?.ToString().Trim() ?? "";
  if (lv.Length > 0 && lv.Length < 120 && !lv.Contains("{")) {
    label = lv; break;
  }

}

Модуль 4: Snippet B — Парсинг ответа ИИ
ИИ ответил. Теперь нужно извлечь из его ответа структурированный план действий. Нейросети любят оборачивать JSON в markdown-блоки, добавлять пояснения до и после, иногда возвращают нестандартные ключи. Snippet B умеет работать с этим всем.

Трёхуровневая защита при парсинге

  • Уровень 1: убираем markdown-обёртки (```json...```), ищем первую сбалансированную пару { }
  • Уровень 2: нормализация action aliases — type→INPUT, press→CLICK, go→NAVIGATE, sleep→WAIT
  • Уровень 3: если координаты x/y=0 — ищем элемент по label в DOM-словаре, исправляем "на лету"
C#:
// Нормализация aliases — как в Python ACT_ALIASES:
System.Func<string, string> normalizeAction = (act) => {
  string a = act.Trim().ToUpper();

  if (a == "TYPE"  || a == "FILL")  return "INPUT";
  if (a == "PRESS" || a == "TAP")   return "CLICK";
  if (a == "GO"    || a == "GOTO")  return "NAVIGATE";
  if (a == "SLEEP" || a == "PAUSE") return "WAIT";
  return a; // CLICK, INPUT, SCROLL, NAVIGATE, WAIT, DONE
};

// Fallback координат через DOM-словарь:
if ((x == 0 || y == 0) && !string.IsNullOrEmpty(label)) {
  string labelLow = label.ToLower().Trim();
  if (domFallbackMap.ContainsKey(labelLow)) {
    x = domFallbackMap[labelLow][0];
    y = domFallbackMap[labelLow][1];
  }
}
Авто-вставка WAIT после каждого CLICK
После парсинга Snippet B автоматически вставляет WAIT(2 секунды) после каждого CLICK, если следующий шаг — не WAIT. Это защищает от ситуации, когда бот кликает кнопку и сразу пытается взаимодействовать с ещё не загрузившимся элементом.


C#:
// Авто-WAIT после CLICK:
if (sAct == "CLICK" && i + 1 < parsedSteps.Count) {
  string nextAct = parsedSteps[i+1]["action"].ToString().ToUpper();
  if (nextAct != "WAIT") {
    finalSteps.Add(new JObject {
      ["action"]   = "WAIT",
      ["wait_sec"] = 2,
      ["reason"]   = "Waiting for DOM after click"
    });
  }
}

Модуль 5: Snippet 2 — Исполнитель действий
Snippet 2 — это "руки" агента. Он берёт нормализованный план и физически выполняет действия в браузере. Но он делает это умнее, чем просто "кликнуть по координатам".

Живой поиск координат через JS
Перед каждым CLICK/INPUT Snippet 2 запускает JS-скрипт поиска элемента по тексту/aria-label/placeholder прямо в реальном DOM. Если находит — использует свежие координаты getBoundingClientRect(). Это резервный механизм на случай, если координаты из plan уже устарели (страница чуть сдвинулась).


C#:
// JS: ищем элемент по label, получаем ЖИВЫЕ координаты
function score(el) {
  var t = (el.innerText||"").toLowerCase();
  var a = (el.getAttribute("aria-label")||"").toLowerCase();
  var p = (el.getAttribute("placeholder")||"").toLowerCase();
  if (a===lLow || t===lLow || p===lLow) return 100;   // точное совпадение
  if (a.indexOf(lLow)>=0 || t.indexOf(lLow)>=0)  return 60;   // содержит
  return 0;
}
// winner: getBoundingClientRect() → cx = left + scrollX + width/2
Ввод текста через SendText: кириллица без проблем
Для INPUT Snippet 2 использует посимвольный instance.SendText() вместо SetValue(). Это решает давнюю боль: обычный SetValue в ZP ломает кириллицу и специальные символы. Дополнительно добавлен fallback через Native Input Events (React/Vue-совместимость через JS nativeInputValueSetter).


C#:
// Посимвольный ввод (кириллица работает):
instance.ActiveTab.KeyboardSimulator.KeyDown(instance.ActiveTab.MainDocument, "Tab");
System.Threading.Thread.Sleep(50);
instance.SendText(execVal, false);  // SendText а не SetValue

// Fallback для React/Vue полей:
string jsNative = "var ta=document.querySelector(...);" +
  "var nativeSetter=Object.getOwnPropertyDescriptor(" +
  "  window.HTMLInputElement.prototype,'value').set;" +
  "nativeSetter.call(ta, " + valJson + ");" +
  "ta.dispatchEvent(new Event('input', {bubbles:true}));";
DOM-diff: измеряем "температуру" изменений
До и после каждого действия Snippet 2 сохраняет снимки DOM (lcm_ai_module_before.txt, lcm_ai_module_after.txt) и вычисляет DOM Change Level:


DOM Change LevelЧто произошло и что делаем
Level 0DOM идентичен — клик, возможно, не сработал → сигнал для верификации RETRY
Level 1Незначительные изменения — INPUT-поля стабильны, CoordRefresh не нужен
Level 2+Страница значительно изменилась — запускаем CoordRefresh для обновления координат

Модуль 6: CoordRefresh — динамическое обновление координат
Это одна из самых элегантных фич системы. Проблема: ИИ спланировал 5 шагов и дал координаты. После первого CLICK страница подгрузилась динамически (выпало меню, открылся accordion, загрузился новый раздел). Координаты шагов 2-5 стали недействительными.

Решение: если DOM Change Level ≥ 2, Snippet 2 автоматически строит CoordRefresh-промпт: список оставшихся шагов со "старыми" координатами → отправляет ИИ → тот возвращает обновлённый action_chain.json с новыми x/y. Цепочка не прерывается, план не пересоздаётся с нуля — просто обновляются координаты.


C#:
// CoordRefresh-промпт (формируется автоматически):
"Page state has changed. The next action requires FRESH coordinates.",
"Current page URL: https://example.com/dashboard/new-section",
"Remaining steps (starting from step 3):",
"  3. CLICK on: Submit button [current coords: 450,320] <- STALE",
"  4. INPUT on: Title field (value: 'My project') [current coords: 440,280] <- STALE",
"Return updated action_chain JSON with FRESH x,y for ALL steps.",

Модуль 7: Self-Verification — агент, который себя проверяет
Это киллер-фича всей архитектуры. Большинство ботов слепы: нажали кнопку → идём дальше. Наш агент после каждого CLICK останавливается и спрашивает ИИ: "То, что я сделал, — сработало?"

Цепочка верификации: Snippet C → D → 3
Snippet C
формирует верификационный промпт. Он передаёт ИИ: действие, которое было выполнено; DOM до и после (diff с новыми/удалёнными элементами); текущий URL и заголовок страницы; автоматические сигналы (DOM unchanged → возможно клик не сработал).

Snippet D парсит ответ верификатора и извлекает JSON с вердиктом. Если JSON нет — ищет ключевые слова ("OK", "RETRY", "ERROR"). Пишет результат в verify_result.txt и обновляет переменные.

Snippet 3 — логика реакции на вердикт. Это сердце State Management.




Вердикт ИИЧто делает Snippet 3
OKСнимает флаг NeedsVerify. Если следующий шаг DONE — завершает цепочку (ChainDone=true)
WAITЖдёт wait_sec секунд, обновляет скриншот и DOM, сигнализирует повторить Snippet C+D
RETRYНаходит упавший шаг, перестраивает task.txt с "Previous attempt failed", сбрасывает план, запускает новый LCM-цикл
ERRORПолный сброс состояния. Планировщик получает контекст ошибки и строит альтернативный маршрут


Вот как выглядит промпт верификатора — посмотрите на его точность:

=== ACTION THAT WAS PERFORMED ===
Step: 3
Action type: CLICK
Target element: "Login button"
Expected result: User should be redirected to dashboard

=== CURRENT PAGE STATE (after action) ===
URL: https://site.com/dashboard
Title: Dashboard — My Account
DOM change level: 2 (significant)

=== AUTOMATIC SIGNALS ===
SIGNAL: DOM значительно изменился (level=2) → действие имело эффект

=== DOM DIFF ===
DOM DIFF: ДО=45 эл., ПОСЛЕ=72 эл.
Появилось: 27 | Исчезло: 3
НОВЫЕ элементы:
+ div|Welcome, User!|
+ button||Logout
+ nav||Dashboard

# Ответ ИИ:
{"verdict":"OK","confidence":"high",
"reason":"Dashboard elements appeared after login click",
"evidence":"nav|Dashboard appeared","wait_sec":0}


Карта переменных и файлов проекта
Переменные ZennoPoster
ПеременнаяНазначение
ChainDone"true" когда вся цепочка выполнена успешно
NeedsVerify"true" после CLICK — сигнал запустить верификацию
VerifyOK"true" если последняя верификация прошла успешно
VerifyMsgСообщение от верификатора (для логов)
CurrentStepNumНомер текущего выполняемого шага
CoordRefreshNeeded"true" если нужно обновить координаты через ИИ
CoordRefreshPromptПромпт для режима CoordRefresh
AiPromptСформированный промпт для отправки ИИ
AiResponseОтвет ИИ из браузера (парсится Snippet B)
VerifyPromptПромпт верификации (отправляется ИИ)
VerifyResponseОтвет ИИ на верификацию (парсится Snippet D)
AiDomTreeСырое дерево DOM от AiDomMapper
AiDomForAIОбработанный DOM от AiDomPreprocessor


Файловая шина данных
ФайлЧто содержит и кто пишет / читает
goal.txt / task.txtТекущая задача (шаг). Пишет: планировщик. Читает: Snippet A
action_chain.jsonНормализованный план от ИИ. Пишет: Snippet B. Читает: Snippet 2, C
next_action.txtПараметры следующего шага (key=value). Пишет: Snippet 2. Читает: ZP
step_status.txtСтатус выполненного шага + URL + DOM level. Пишет: Snippet 2. Читает: Snippet C
verify_result.txtВердикт верификации (VERIFY_STATUS, VERIFY_MSG...). Пишет: Snippet D. Читает: Snippet 3
dom_input.txtТекущий DOM (JSON). Пишет: AiDomPreprocessor. Читает: Snippet A, B
lcm_ai_module_before.txtDOM до действия. Пишет: Snippet 2. Читает: Snippet C
lcm_ai_module_after.txtDOM после действия. Пишет: Snippet 2. Читает: Snippet C, A-Refresh
context_store.jsonНакопленный контекст (логины, сохранённые значения). Пишет: Snippet 2 (save). Читает: Snippet A
ai_prompt.txtИтоговый промпт. Пишет: Snippet A/A-Refresh. Читает: ZP (вставка в браузер)

Практика: реальные примеры задач
Пример 1: Автоматический постинг через веб-интерфейс GPT
Цель: каждый день в 9:00 открыть ChatGPT, отправить промпт для генерации поста, скопировать ответ, перейти на сайт клиента и опубликовать.

# goal.txt:
Open ChatGPT, send the daily post prompt from prompt.txt,
copy the response, navigate to client-site.com/admin/posts,
create new post, paste the content and publish.

# Планировщик разобьёт это на:
01. Navigate to https://chatgpt.com
02. [AFTER_NAV] Click the New Chat button
03. [AFTER_NAV] Type the prompt text into the message input
04. [AFTER_NAV] Click the Send button
05. [AFTER_NAV] Wait for response and copy it
06. Navigate to https://client-site.com/admin/posts/new
07. [AFTER_NAV] Click into the Title field and type title
08. [AFTER_NAV] Click into Content area and paste
09. [AFTER_NAV] Click the Publish button


Пример 2: Мониторинг с анализом через Claude
Агент открывает страницу конкурента, скриншотит, передаёт DOM в Claude через вторую вкладку, получает анализ изменений цен/акций, записывает в таблицу.

Пример 3: Заполнение форм с CAPTCHA-логикой
Агент заполняет форму регистрации. Если верификация возвращает ERROR с "captcha" в evidence — Snippet 3 передаёт задачу captcha-решателю и повторяет цепочку с нового шага. Бот не теряет заполненные поля — контекст сохранён в context_store.json.


Сравнение: наш подход vs классические решения

КритерийНаш агент vs. традиционный бот / прямой API
Стоимость запросовПодписка на веб-сервис (~$20/мес) vs. API (~$50-500/мес при интенсивном использовании)
Устойчивость к редизайнуВысокая: ИИ анализирует DOM заново vs. Низкая: XPath/CSS ломаются при обновлениях
Обработка динамикиCoordRefresh + верификация vs. Нет: бот кликает в пустоту
Self-recoveryЕсть: RETRY/ERROR запускают перепланирование vs. Нет: бот зависает
Требует API-ключиНет vs. Да
МультиокончностьДа: несколько ИИ в разных вкладках vs. Нет
Сложность настройкиВысокая (один раз) vs. Средняя
Надёжность при стабильных сайтахВысокая vs. Высокая (если сайт не меняется)

Тонкости реализации и грабли
1. UTF-8 ↔ Latin-1 в ZennoPoster
GetAttribute в ZP возвращает UTF-8 байты интерпретированные как Latin-1. Для кириллицы это катастрофа — вместо букв получаем мусор. Решение: читаем каждый char как байт (& 0xFF) и пересоздаём строку через Encoding.UTF8.GetString:

// Исправление кодировки в C#:


C#:
byte[] utf8bytes = new byte[rawResult.Length];
for (int k = 0; k < rawResult.Length; k++)
    utf8bytes[k] = (byte)(rawResult[k] & 0xFF);
string finalJson = System.Text.Encoding.UTF8.GetString(utf8bytes);
2. Shadow DOM и iframe
Современные сайты активно используют Shadow DOM (веб-компоненты) и iframe. Стандартный querySelector их не видит. AI DOM Mapper обходит это через рекурсивный обход shadowRoot и постановку iframe-контентов в очередь для отдельного сканирования.

3. React/Vue: нативные события
Многие современные фреймворки не реагируют на SetValue — им нужен нативный ввод с InputEvent. Snippet 2 после SetValue запускает JS nativeInputValueSetter с dispatchEvent(new Event("input", {bubbles:true})) — поле "видит" изменение и обновляет state.

4. Гонка состояний при быстрых страницах
Если страница загружается быстрее, чем ZP успевает снять DOM — получаем неполную картину. Защита: после каждого действия добавляем WAIT(2s), и DOM-сканирование делается только после проверки ReadyState документа.

5. Токен-менеджмент промпта
Даже после фильтрации DOM может оказаться большим. Snippet A обрезает его до 18 000 символов если после фильтрации ничего не осталось. Для промпта верификации Snippet C показывает максимум 6 новых элементов и 4 удалённых — этого всегда достаточно для верного вердикта.



Дорожная карта: куда развивать систему
Краткосрочные улучшения (готовы к реализации)
  • Визуальная верификация через скриншоты: передавать screenshot_before + screenshot_after в мультимодальный ИИ вместо DOM-diff — более человекоподобный контроль
  • Параллельные цепочки: несколько вкладок ZP выполняют независимые цепочки одновременно, планировщик координирует
  • Контекстная память через векторную БД: хранить историю успешных паттернов, подсказывать ИИ "на этом сайте кнопка Submit всегда в правом нижнем углу"
  • Веб-интерфейс планировщика: Flask/FastAPI дашборд для мониторинга прогресса цепочек в реальном времени
Среднесрочные возможности
  • Поддержка локальных LLM без Ollama: llama.cpp с прямым HTTP-интерфейсом как замена облачным ИИ
  • Автоматический retrain промптов: если определённый паттерн верификации часто возвращает RETRY — система автоматически корректирует system prompt
  • Интеграция с n8n/Make: планировщик как webhook, который может управляться из внешних автоматизаций

Заключение
То, что мы разобрали — это не просто "бот, который открывает ChatGPT". Это полноценная агентная архитектура с управлением состоянием, которая решает три главные проблемы классической автоматизации: зависимость от вёрстки, слепоту к результату и хрупкость при неожиданных изменениях.

Ключевые идеи, которые делают эту систему особенной:


  • Атомарное планирование: 1 шаг = 1 запрос к ИИ — никаких галлюцинаций при длинных цепочках
  • DOM как структурированный контекст: не скриншоты, не сырой HTML — компактный JSON с координатами
  • Self-verification с рефлексией: агент знает, сработало ли его действие, и может перестроить план
  • CoordRefresh: динамические страницы не ломают цепочку — координаты обновляются на лету
  • Файловая шина: состояние хранится в файлах, а не в памяти — отладка и воспроизведение тривиальны


Все исходники в одном месте
task_chain_planner.py — Python-планировщик с разбивкой цели на шаги
Python:
"""
================================================================
TASK CHAIN PLANNER v1.0
Разбивает цель автоматизации на цепочку одиночных промптов
Совместим с logic_control_module.py + step_verifier.py
================================================================

ИСПОЛЬЗОВАНИЕ:
python task_chain_planner.py
или задать GOAL прямо в скрипте

РЕЗУЛЬТАТ:
task_step_01.txt — первый шаг (записывается в task.txt автоматически)
task_step_02.txt — второй шаг
...
task_plan.json — полный план для контроля
task.txt — текущий активный шаг (читается logic_control_module.py)

ЛОГИКА:
1. Принимает высокоуровневую цель
2. LLM разбивает её на атомарные шаги (1 действие = 1 файл)
3. Каждый шаг — точный промпт для logic_control_module.py
4. ZennoPoster/LCM читает task.txt → выполняет → плановщик
записывает следующий шаг в task.txt
================================================================
"""

import os
import re
import sys
import json
import logging
from datetime import datetime
from typing import Optional

import ollama

# ------------------------------------------------------------------
# PATHS
# ------------------------------------------------------------------
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
STEPS_DIR = os.path.join(BASE_DIR, "task_chain") # папка для шагов и плана
os.makedirs(STEPS_DIR, exist_ok=True)

TASK_FILE = os.path.join(BASE_DIR, "goal.txt") # читается LCM (рядом со скриптом)
PLAN_FILE = os.path.join(STEPS_DIR, "task_plan.json") # полный план
PLAN_LOG = os.path.join(STEPS_DIR, "planner.log")
CONTEXT_FILE = os.path.join(BASE_DIR, "context_store.json")
STEP_STATUS_FILE = os.path.join(BASE_DIR, "step_status.txt") # пишет ZennoPoster

# ------------------------------------------------------------------
# LOGGING
# ------------------------------------------------------------------
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)-8s | %(message)s",
datefmt="%H:%M:%S",
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler(PLAN_LOG, encoding="utf-8"),
],
)
log = logging.getLogger("PLANNER")

# ------------------------------------------------------------------
# CONFIG
# ------------------------------------------------------------------
MODEL_NAME = "llama3" # та же модель что и в LCM


# ------------------------------------------------------------------
# PLANNER SYSTEM PROMPT
# ------------------------------------------------------------------
PLANNER_SYSTEM_PROMPT = """\
You are a web automation task decomposer for ZennoPoster AI agent.

Your job: take a HIGH-LEVEL GOAL and break it into ATOMIC STEPS.
Each step must be a SINGLE browser action that logic_control_module.py can execute.

RULES FOR EACH STEP:
1. One step = one atomic action (one click, one input, one navigation).
2. Each step description must be a SHORT, PRECISE instruction in English.
3. Format: imperative sentence. Example: "Click the New Chat button"
4. If a step requires navigating first, split into: "Navigate to X" then "Click Y".
5. If a step's target element may only appear AFTER a previous step executes
(e.g. after opening a new chat), mark it with prefix: [AFTER_NAV]
Example: "[AFTER_NAV] Click the file attachment button (+) in the chat input area"
6. Steps marked [AFTER_NAV] will trigger DOM_RESCAN automatically.
7. Keep total steps between 1 and 10.
8. Do NOT include verification steps — that's handled separately.

OUTPUT FORMAT — raw JSON only, no markdown:
{
"goal": "<original goal>",
"total_steps": <int>,
"steps": [
{
"step_num": 1,
"prompt": "<exact instruction for LCM>",
"after_nav": false,
"expected_result": "<what should happen>",
"site_hint": "<website or URL if known, else empty>"
}
]
}
"""


# ------------------------------------------------------------------
# CONTEXT READER
# ------------------------------------------------------------------
def _read_context() -> str:
if not os.path.exists(CONTEXT_FILE):
return "No previous context."
try:
with open(CONTEXT_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
if not data:
return "No previous context."
lines = []
for k, v in data.items():
lines.append(f" {k}: {v.get('value', '')} (saved at {v.get('saved_at', '')})")
return "\n".join(lines)
except Exception:
return "No previous context."


# ------------------------------------------------------------------
# STEP FILE WRITER
# ------------------------------------------------------------------
def _write_step_file(step_num: int, prompt: str, after_nav: bool,
expected: str, site_hint: str) -> str:
"""Writes task_step_NN.txt and returns the filepath."""
filename = f"task_step_{step_num:02d}.txt"
filepath = os.path.join(STEPS_DIR, filename)

lines = [
f"STEP_NUM={step_num}",
f"AFTER_NAV={'true' if after_nav else 'false'}",
f"SITE_HINT={site_hint}",
f"EXPECTED={expected}",
f"PROMPT={prompt}",
]
with open(filepath, "w", encoding="utf-8") as f:
f.write("\n".join(lines))

log.info(f"Written: {filename} | after_nav={after_nav} | '{prompt[:60]}'")
return filepath


# ------------------------------------------------------------------
# ACTIVATE STEP — записывает нужный шаг в task.txt
# ------------------------------------------------------------------
def activate_step(step_num: int, plan: dict) -> bool:
"""
Записывает промпт шага N в task.txt (который читает logic_control_module.py).
Если шаг помечен after_nav=True — добавляет контекст "already on page".
"""
steps = plan.get("steps", [])
step = next((s for s in steps if s["step_num"] == step_num), None)
if not step:
log.error(f"Step #{step_num} not found in plan.")
return False

prompt = step["prompt"]

# Для after_nav шагов — нужно добавить контекст текущего URL
if step.get("after_nav", False):
# Читаем URL из step_status.txt если есть
current_url = ""
if os.path.exists(STEP_STATUS_FILE):
try:
with open(STEP_STATUS_FILE, "r", encoding="utf-8") as f:
for line in f:
if line.strip().startswith("CURRENT_URL="):
current_url = line.strip().split("=", 1)[1]
break
except Exception:
pass

if current_url:
prompt = (
f"The browser is already on page: {current_url}. "
f"Find and click the element: {prompt.replace('[AFTER_NAV] ', '')}. "
f"Do NOT navigate away or click New Chat. Use ONLY coordinates from the current DOM."
)
else:
prompt = prompt.replace("[AFTER_NAV] ", "")

with open(TASK_FILE, "w", encoding="utf-8") as f:
f.write(prompt)

log.info(f"task.txt activated: Step #{step_num} | '{prompt[:80]}'")
return True


# ------------------------------------------------------------------
# PLAN GENERATOR
# ------------------------------------------------------------------
def generate_plan(goal: str) -> Optional[dict]:
"""Calls LLM to decompose goal into atomic steps."""
context_summary = _read_context()

user_prompt = (
f"GOAL: {goal}\n\n"
f"SAVED CONTEXT (from previous sessions):\n{context_summary}\n\n"
f"Break this goal into atomic browser automation steps. "
f"Output ONLY valid JSON as specified."
)

log.info(f"Generating plan for goal: '{goal}'")

try:
client = ollama.Client()
resp = client.chat(
model=MODEL_NAME,
format="json",
messages=[
{"role": "system", "content": PLANNER_SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
],
options={"temperature": 0.1},
)
if hasattr(resp, "message"):
raw = resp.message.content
elif isinstance(resp, dict):
raw = resp.get("message", {}).get("content", "")
else:
raw = str(resp)

log.info(f"LLM response (first 500):\n{raw[:500]}")

except Exception as e:
log.error(f"LLM error: {e}")
return None

# Parse JSON
raw = re.sub(r"```(?:json)?", "", raw).strip()
data = None
for m in re.finditer(r"\{", raw):
candidate = raw[m.start():]
depth, end = 0, -1
for i, ch in enumerate(candidate):
if ch == "{": depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
end = i + 1
break
if end == -1:
continue
try:
data = json.loads(candidate[:end])
break
except json.JSONDecodeError:
continue

if not data or "steps" not in data:
log.error("Failed to parse plan from LLM response.")
log.error(f"Raw: {raw[:800]}")
return None

# Normalize after_nav from [AFTER_NAV] prefix in prompt
for s in data["steps"]:
if s.get("prompt", "").startswith("[AFTER_NAV]"):
s["after_nav"] = True
s["prompt"] = s["prompt"].replace("[AFTER_NAV] ", "").strip()
else:
s["after_nav"] = s.get("after_nav", False)

log.info(f"Plan generated: {len(data['steps'])} steps")
return data


# ------------------------------------------------------------------
# SAVE PLAN
# ------------------------------------------------------------------
def save_plan(plan: dict):
plan["generated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(PLAN_FILE, "w", encoding="utf-8") as f:
json.dump(plan, f, ensure_ascii=False, indent=2)
log.info(f"Plan saved to task_plan.json")


# ------------------------------------------------------------------
# LOAD PLAN
# ------------------------------------------------------------------
def load_plan() -> Optional[dict]:
if not os.path.exists(PLAN_FILE):
return None
try:
with open(PLAN_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
log.error(f"Failed to load plan: {e}")
return None


# ------------------------------------------------------------------
# PRINT PLAN SUMMARY
# ------------------------------------------------------------------
def print_plan(plan: dict):
SEP = "=" * 68
print(f"\n{SEP}")
print(f" GOAL : {plan.get('goal', '')}")
print(f" TOTAL STEPS: {plan.get('total_steps', len(plan.get('steps', [])))}")
print(f" GENERATED : {plan.get('generated_at', '')}")
print(SEP)
for s in plan.get("steps", []):
nav_tag = " [AFTER_NAV]" if s.get("after_nav") else ""
print(f" {s['step_num']:02d}.{nav_tag} {s['prompt']}")
if s.get("expected_result"):
print(f" → {s['expected_result']}")
fname = f"task_step_{s['step_num']:02d}.txt"
print(f" {fname}")
print(SEP)
print(f" task.txt : Step 01 is now active\n")


# ------------------------------------------------------------------
# MAIN
# ------------------------------------------------------------------
def main():
# ---- Режим: продолжить план или создать новый -------------------
if "--next" in sys.argv:
# Режим: активировать следующий шаг (вызывается ZennoPoster после верификации)
plan = load_plan()
if not plan:
log.error("No task_plan.json found. Run planner first.")
sys.exit(1)

# Определяем текущий шаг из step_status.txt
current_step = 0
if os.path.exists(STEP_STATUS_FILE):
try:
with open(STEP_STATUS_FILE, "r", encoding="utf-8") as f:
for line in f:
if line.strip().startswith("STEP_NUM="):
current_step = int(line.strip().split("=", 1)[1])
break
except Exception:
pass

next_step = current_step + 1
total = plan.get("total_steps", len(plan.get("steps", [])))

if next_step > total:
log.info("All steps completed! Writing DONE to task.txt")
with open(TASK_FILE, "w", encoding="utf-8") as f:
f.write("DONE")
print("\n✅ ALL STEPS COMPLETED")
sys.exit(0)

success = activate_step(next_step, plan)
if success:
print(f"\n▶ Activated step #{next_step}: {plan['steps'][next_step-1]['prompt'][:60]}")
sys.exit(0 if success else 1)

elif "--activate" in sys.argv:
# Режим: активировать конкретный шаг: python planner.py --activate 3
idx = sys.argv.index("--activate")
step_num = int(sys.argv[idx + 1]) if idx + 1 < len(sys.argv) else 1
plan = load_plan()
if not plan:
log.error("No task_plan.json found.")
sys.exit(1)
activate_step(step_num, plan)
sys.exit(0)

elif "--show" in sys.argv:
# Режим: показать текущий план
plan = load_plan()
if plan:
print_plan(plan)
else:
print("No plan found.")
sys.exit(0)

else:
# ---- Основной режим: создать новый план ---------------------

# Цель — из аргумента командной строки или из файла goal.txt или хардкод
GOAL = " "

goal_file = os.path.join(BASE_DIR, "goal.txt")
if os.path.exists(goal_file):
with open(goal_file, "r", encoding="utf-8") as f:
g = f.read().strip()
if g:
GOAL = g
log.info(f"Goal loaded from goal.txt: {GOAL}")

if len(sys.argv) > 1 and not sys.argv[1].startswith("--"):
GOAL = " ".join(sys.argv[1:])
log.info(f"Goal from CLI: {GOAL}")

# 1. Генерируем план
plan = generate_plan(GOAL)
if not plan:
log.error("Plan generation failed.")
sys.exit(1)

# 2. Сохраняем task_step_NN.txt для каждого шага
for step in plan["steps"]:
_write_step_file(
step_num = step["step_num"],
prompt = ("[AFTER_NAV] " if step.get("after_nav") else "") + step["prompt"],
after_nav = step.get("after_nav", False),
expected = step.get("expected_result", ""),
site_hint = step.get("site_hint", ""),
)

# 3. Сохраняем полный план
save_plan(plan)

# 4. Активируем первый шаг в task.txt
activate_step(1, plan)

# 5. Выводим сводку
print_plan(plan)

log.info("Planner complete. task.txt is ready for logic_control_module.py")
sys.exit(0)


if __name__ == "__main__":
main()
Snippet 1 (скрипт сброса состояния) — инициализация новой цепочки
C#:
// =====================================================================
// SNIPPET 1 — Chain Initializer v2
// Полный сброс состояния перед стартом новой цепочки
// Результат: "SUCCESS" — цепочка готова к выполнению
// "ERROR_*" — что-то пошло не так
// =====================================================================
string BOT_DIR = project.Directory + @"\";
string CHAIN_FILE = BOT_DIR + "action_chain.json";
string NEXT_FILE = BOT_DIR + "next_action.txt";
string STATUS_FILE = BOT_DIR + "step_status.txt";

if (!System.IO.File.Exists(CHAIN_FILE)) {
project.SendErrorToLog("action_chain.json не найден! Сначала запустите logic_control_module.py", true);
return "ERROR_NO_CHAIN_FILE";
}

try {
// =====================================================================
// ПОЛНЫЙ СБРОС СОСТОЯНИЯ — критично для корректного старта новой цепочки
// =====================================================================

// Удаляем файлы прошлого прогона
foreach (var f in new[] { STATUS_FILE, NEXT_FILE, BOT_DIR + "verify_result.txt" }) {
if (System.IO.File.Exists(f)) System.IO.File.Delete(f);
}

// Сбрасываем ВСЕ переменные состояния
project.Variables["ChainDone"].Value = "false";
project.Variables["NeedsVerify"].Value = "false";
project.Variables["VerifyOK"].Value = "false";
project.Variables["VerifyMsg"].Value = "";
project.Variables["CurrentStepNum"].Value = "1"; // <-- ключевой сброс
project.Variables["CurrentAction"].Value = "";
project.Variables["CurrentLabel"].Value = "";
project.Variables["CurrentValue"].Value = "";
project.Variables["CurrentX"].Value = "0";
project.Variables["CurrentY"].Value = "0";
project.Variables["CurrentWaitSec"].Value = "0";
project.Variables["CurrentSaveFrom"].Value= "none";
project.Variables["CurrentSaveKey"].Value = "";
project.Variables["NextStepNum"].Value = "0";
project.Variables["NextAction"].Value = "";

// Необязательные переменные — не падаем если нет
try { project.Variables["ChainResult"].Value = ""; } catch { }
try { project.Variables["ChainNeedVerify"].Value = "false"; } catch { }

// =====================================================================
// Парсим JSON и записываем шаг 1 в next_action.txt
// =====================================================================
string jsonText = System.IO.File.ReadAllText(CHAIN_FILE, System.Text.Encoding.UTF8);
var jObj = Global.ZennoLab.Json.Linq.JObject.Parse(jsonText);
var steps = jObj["steps"] as Global.ZennoLab.Json.Linq.JArray;

if (steps == null || steps.Count == 0) {
project.SendErrorToLog("action_chain.json: пустой массив шагов!", true);
return "ERROR_EMPTY_STEPS";
}

// Берём шаг №1
Global.ZennoLab.Json.Linq.JToken firstStep = null;
foreach (var step in steps) {
if (step["step_num"] != null && step["step_num"].ToString() == "1") {
firstStep = step;
break;
}
}
if (firstStep == null) firstStep = steps[0];

System.Func<string, string, string> getStr = (key, def) =>
firstStep[key] != null ? firstStep[key].ToString() : def;

var lines = new System.Collections.Generic.List<string>();
lines.Add("ACTION=" + getStr("action", "ERROR").ToUpper());
lines.Add("STEP_NUM=1");
lines.Add("LABEL=" + getStr("label", ""));
lines.Add("X=" + getStr("x", "0"));
lines.Add("Y=" + getStr("y", "0"));
lines.Add("VALUE=" + getStr("value", ""));
lines.Add("SCROLL_DIR=" + getStr("scroll_dir", "down"));
lines.Add("SCROLL_PX=" + getStr("scroll_px", "300"));
lines.Add("WAIT_SEC=" + getStr("wait_sec", "0"));
lines.Add("SAVE_KEY=" + getStr("save_key", ""));
lines.Add("SAVE_FROM=" + getStr("save_from", "none"));
lines.Add("SAVE_HINT=" + getStr("save_hint", ""));

System.IO.File.WriteAllLines(NEXT_FILE, lines.ToArray(), new System.Text.UTF8Encoding(false));

string taskSummary = jObj["task_summary"] != null ? jObj["task_summary"].ToString() :
(jObj["task"] != null ? jObj["task"].ToString() : "—");
int totalSteps = steps.Count;

project.SendInfoToLog(
"Новая цепочка запущена | Задача: " + taskSummary +
" | Шагов: " + totalSteps, true);

return "SUCCESS";

} catch (System.Exception ex) {
project.SendErrorToLog("Ошибка инициализации цепочки: " + ex.Message, true);
return "JSON_PARSE_ERROR";
}
AI DOM Mapper / AI DOM Preprocessor — "глаза" агента
AI DOM Mapper:
// ==============================================================================
// Скрипт: Универсальный AI DOM Mapper для ZennoPoster
// Версия: 2.0 — Полная карта страницы для ИИ-агента
// Собирает ВСЕ элементы с координатами, размерами и всеми доступными атрибутами
// Включает: Light DOM + Shadow DOM + iframe (если доступен)
// ==============================================================================

// Проверяем наличие переменной в проекте
if (!project.Variables.Keys.Contains("AiDomTree"))
{
project.SendErrorToLog(
"КРИТИЧЕСКАЯ ОШИБКА: Создайте переменную 'AiDomTree' во вкладке 'Переменные' проекта!",
true
);
throw new Exception("Переменная AiDomTree отсутствует в проекте.");
}

try
{
// --- Ожидаем полной загрузки страницы ---
if (instance.ActiveTab.IsBusy)
instance.ActiveTab.WaitDownloading();

System.Threading.Thread.Sleep(1500);

if (instance.ActiveTab.IsVoid || instance.ActiveTab.IsNull || instance.ActiveTab.MainDocument == null)
throw new Exception("Вкладка или документ не инициализированы.");

var bodyCheck = instance.ActiveTab.FindElementByXPath("//body", 0);
if (bodyCheck.IsVoid)
throw new Exception("Тег <body> не найден. Страница пуста или не успела отрендериться.");

// ===========================================================
// ГЛАВНЫЙ JS-СКРИПТ: Сбор всех элементов и их атрибутов
// ===========================================================
string jsScript = @"
(function() {
try {
if (!document.body) return 'Error: no body';

// Теги, которые не несут визуальной ценности для ИИ-агента
const IGNORE_TAGS = new Set([
'SCRIPT','STYLE','NOSCRIPT','META','HEAD','LINK',
'PATH','DEFS','SVG','BR','WBR','TEMPLATE'
]);

// Интерактивные теги / роли
const INTERACTIVE_TAGS = new Set([
'A','BUTTON','INPUT','SELECT','TEXTAREA',
'DETAILS','SUMMARY','LABEL','OPTION','OPTGROUP'
]);

const INTERACTIVE_ROLES = new Set([
'button','link','menuitem','tab','checkbox','radio',
'switch','combobox','listbox','option','treeitem',
'slider','spinbutton','searchbox','textbox','gridcell'
]);

let elements = [];
let idCounter = 0;

// -------------------------------------------------------
// Вспомогательные функции
// -------------------------------------------------------
function safeStr(v, max) {
if (!v) return undefined;
let s = String(v).replace(/\s+/g,' ').trim();
return s.length ? s.substring(0, max || 300) : undefined;
}

function getRect(el) {
try {
const r = el.getBoundingClientRect();
return {
x: Math.round(r.left + window.scrollX),
y: Math.round(r.top + window.scrollY),
w: Math.round(r.width),
h: Math.round(r.height),
cx: Math.round(r.left + window.scrollX + r.width / 2),
cy: Math.round(r.top + window.scrollY + r.height / 2)
};
} catch(e) {
return { x:0, y:0, w:0, h:0, cx:0, cy:0 };
}
}

function isVisible(rect) {
return rect.w > 0 && rect.h > 0;
}

function getComputedProps(el) {
try {
const cs = window.getComputedStyle(el);
return {
display: cs.display,
visibility: cs.visibility,
opacity: cs.opacity,
zIndex: cs.zIndex !== 'auto' ? cs.zIndex : undefined,
position: cs.position !== 'static' ? cs.position : undefined,
cursor: cs.cursor !== 'auto' ? cs.cursor : undefined,
fontSize: cs.fontSize,
color: cs.color,
bgColor: cs.backgroundColor !== 'rgba(0, 0, 0, 0)' ? cs.backgroundColor : undefined,
overflow: (cs.overflow !== 'visible') ? cs.overflow : undefined
};
} catch(e) { return {}; }
}

// -------------------------------------------------------
// Основная функция обхода DOM
// -------------------------------------------------------
function traverse(el, depth, shadowDepth, parentId) {
if (!el || el.nodeType !== 1) return;

let tag = el.tagName ? el.tagName.toUpperCase() : '';
if (IGNORE_TAGS.has(tag)) return;
// Пропускаем служебные элементы самого скрипта
if (el.id && el.id.startsWith('zp_')) return;

let rect = getRect(el);
let nodeId = ++idCounter;

// --- Определяем тип элемента ---
let role = el.getAttribute('role') || '';
let type = el.getAttribute('type') || '';

let isInteractive = INTERACTIVE_TAGS.has(tag)
|| INTERACTIVE_ROLES.has(role)
|| el.getAttribute('onclick') != null
|| el.getAttribute('tabindex') != null
|| el.contentEditable === 'true';

let isImage = tag === 'IMG' || tag === 'PICTURE' || tag === 'CANVAS' || tag === 'VIDEO';
let isText = tag === 'P' || tag === 'H1' || tag === 'H2' || tag === 'H3'
|| tag === 'H4' || tag === 'H5' || tag === 'H6' || tag === 'SPAN'
|| tag === 'LI' || tag === 'TD' || tag === 'TH' || tag === 'CAPTION'
|| tag === 'BLOCKQUOTE' || tag === 'FIGCAPTION' || tag === 'LABEL';
let isForm = tag === 'FORM';
let isNav = tag === 'NAV' || tag === 'HEADER' || tag === 'FOOTER'
|| tag === 'ASIDE' || tag === 'MAIN' || tag === 'SECTION' || tag === 'ARTICLE';

// --- Собираем объект ---
let obj = {
id: nodeId,
parentId: parentId || null,
tag: tag.toLowerCase(),
depth: depth
};

if (shadowDepth > 0) obj.shadowDepth = shadowDepth;

// Позиция и размеры — САМОЕ ВАЖНОЕ для ИИ-агента
obj.x = rect.x;
obj.y = rect.y;
obj.w = rect.w;
obj.h = rect.h;
obj.cx = rect.cx; // центр X — удобно для клика
obj.cy = rect.cy; // центр Y — удобно для клика

obj.visible = isVisible(rect);

// --- Тип узла ---
if (isInteractive) obj.interactive = true;
if (isImage) obj.isImage = true;
if (isText) obj.isText = true;
if (isForm) obj.isForm = true;
if (isNav) obj.isNav = true;

// --- Текстовое содержимое ---
let innerText = safeStr(el.innerText || el.textContent, 400);
if (innerText) obj.text = innerText;

// --- Атрибуты ---
if (el.id) obj.elId = el.id;
if (el.name) obj.name = el.name;
if (el.className && typeof el.className === 'string')
obj.cls = safeStr(el.className, 150);
if (el.href) obj.href = safeStr(el.href, 300);
if (el.src) obj.src = safeStr(el.src, 300);
if (el.alt) obj.alt = safeStr(el.alt, 200);
if (el.title) obj.title = safeStr(el.title,200);
if (el.placeholder) obj.ph = safeStr(el.placeholder, 200);
if (el.value !== undefined && el.value !== '')
obj.value = safeStr(String(el.value), 200);
if (el.checked !== undefined) obj.checked = el.checked;
if (el.disabled) obj.disabled= true;
if (el.readOnly) obj.readOnly= true;
if (el.required) obj.required= true;
if (role) obj.role = role;
if (type) obj.type = type;

// aria-атрибуты
let ariaLabel = el.getAttribute('aria-label');
let ariaDesc = el.getAttribute('aria-describedby');
let ariaExp = el.getAttribute('aria-expanded');
let ariaHid = el.getAttribute('aria-hidden');
let ariaLive = el.getAttribute('aria-live');
if (ariaLabel) obj.ariaLabel = safeStr(ariaLabel, 200);
if (ariaDesc) obj.ariaDesc = ariaDesc;
if (ariaExp) obj.ariaExpanded = ariaExp;
if (ariaHid) obj.ariaHidden = ariaHid;
if (ariaLive) obj.ariaLive = ariaLive;

// data-атрибуты (первые 5, полезны для ИИ)
try {
let dKeys = Object.keys(el.dataset || {}).slice(0, 5);
if (dKeys.length > 0) {
obj.data = {};
dKeys.forEach(k => { obj.data[k] = safeStr(el.dataset[k], 100); });
}
} catch(e) {}

// Стили (только для интерактивных/важных)
if (isInteractive || isImage) {
let cp = getComputedProps(el);
if (cp.display !== 'block' && cp.display) obj.display = cp.display;
if (cp.visibility !== 'visible') obj.visibility = cp.visibility;
if (cp.opacity !== '1' && cp.opacity) obj.opacity = cp.opacity;
if (cp.position) obj.position = cp.position;
if (cp.cursor && cp.cursor !== 'default') obj.cursor = cp.cursor;
if (cp.zIndex) obj.zIndex = cp.zIndex;
if (cp.fontSize) obj.fontSize = cp.fontSize;
if (cp.bgColor) obj.bgColor = cp.bgColor;
}

// --- ZennoPoster-совместимые поля ---
// leftInTab / topInTab (синонимы x/y в системе координат вкладки)
obj.leftInTab = rect.x;
obj.topInTab = rect.y;
obj.clientWidth = rect.w;
obj.clientHeight = rect.h;

// outerHTML (только для интерактивных, первые 500 символов)
if (isInteractive) {
try {
obj.outerHtml = el.outerHTML.substring(0, 500);
} catch(e) {}
}

elements.push(obj);

// --- Рекурсия: Light DOM ---
if (el.children) {
for (let i = 0; i < el.children.length; i++) {
traverse(el.children[i], depth + 1, shadowDepth, nodeId);
}
}

// --- Рекурсия: Shadow DOM ---
if (el.shadowRoot && el.shadowRoot.children) {
for (let i = 0; i < el.shadowRoot.children.length; i++) {
traverse(el.shadowRoot.children[i], depth + 1, shadowDepth + 1, nodeId);
}
}
}

traverse(document.body, 0, 0, null);

// -------------------------------------------------------
// Строим итоговый JSON с метаданными страницы
// -------------------------------------------------------
let meta = {
url: window.location.href,
title: document.title,
scrollX: window.scrollX,
scrollY: window.scrollY,
viewportW: window.innerWidth,
viewportH: window.innerHeight,
pageW: document.documentElement.scrollWidth,
pageH: document.documentElement.scrollHeight,
totalNodes: elements.length,
timestamp: new Date().toISOString()
};

// Краткий индекс интерактивных элементов (для быстрого поиска ИИ)
let interactive = elements.filter(e => e.interactive && e.visible);
let images = elements.filter(e => e.isImage && e.visible);

const result = JSON.stringify({
meta: meta,
elements: elements,
interactive: interactive,
images: images
});

// Сохраняем в скрытый textarea
let old = document.getElementById('zp_ai_dom_result');
if (old) old.remove();
let ta = document.createElement('textarea');
ta.id = 'zp_ai_dom_result';
ta.style.display = 'none';
ta.value = result;
document.body.appendChild(ta);

return 'OK:' + elements.length;

} catch(e) {
return 'JSERROR: ' + e.message + ' | ' + e.stack;
}
})();
";

// --- Выполняем JS ---
string jsResult = instance.ActiveTab.MainDocument.EvaluateScript(jsScript);

if (jsResult == null || jsResult.StartsWith("JSERROR"))
throw new Exception($"Ошибка выполнения JS: {jsResult}");

project.SendInfoToLog($"JS выполнен: {jsResult}", true);

// --- Ждём появления результата в DOM ---
var resultEl = instance.ActiveTab.FindElementById("zp_ai_dom_result");
int wait = 0;
while (resultEl.IsVoid && wait < 50)
{
System.Threading.Thread.Sleep(100);
resultEl = instance.ActiveTab.FindElementById("zp_ai_dom_result");
wait++;
}

if (resultEl.IsVoid)
throw new Exception("Результат не появился в DOM (таймаут).");

// --- Читаем JSON ---
string json = resultEl.GetValue();

// Удаляем служебный элемент
instance.ActiveTab.MainDocument.EvaluateScript(
"var e=document.getElementById('zp_ai_dom_result'); if(e) e.remove();"
);

if (string.IsNullOrWhiteSpace(json) || json == "null")
throw new Exception("Полученный JSON пустой.");

// --- Сохраняем в переменную проекта ---
project.Variables["AiDomTree"].Value = json;

// --- Краткая статистика в лог ---
// Быстрый парсинг totalNodes без внешней библиотеки
int totalIdx = json.IndexOf("\"totalNodes\":");
string totalStr = "?";
if (totalIdx >= 0)
{
int start = totalIdx + 13;
int end = json.IndexOfAny(new char[]{',','}'}, start);
if (end > start) totalStr = json.Substring(start, end - start).Trim();
}

project.SendInfoToLog(
$"✅ DOM-карта готова! Узлов: {totalStr} | Размер JSON: {json.Length} символов",
true
);

return "Success";
}
catch (Exception ex)
{
project.SendErrorToLog($"❌ AiDomMapper ОШИБКА: {ex.Message}", true);
return null;
}
AI DOM Preprocessor:
// ==============================================================================
// AI DOM Preprocessor v8
// EvaluateScript НЕ возвращает значения в этой версии ZP — только пишем в DOM
// Схема: C# -> base64 чанки -> JS атрибуты span[data-N] -> JS обрабатывает ->
// JS пишет результат в span[data-r-N] -> C# читает GetAttribute
// ==============================================================================

if (!project.Variables.Keys.Contains("AiDomTree") ||
string.IsNullOrWhiteSpace(project.Variables["AiDomTree"].Value))
{
project.SendErrorToLog("AiDomTree пуста. Сначала запустите AiDomMapper.", true);
return null;
}
if (!project.Variables.Keys.Contains("AiDomForAI"))
{
project.SendErrorToLog("Создайте переменную 'AiDomForAI' в проекте!", true);
return null;
}

try
{
string rawJson = project.Variables["AiDomTree"].Value;

// base64 — безопасен в HTML-атрибутах (только A-Z a-z 0-9 + / =)
string base64Json = System.Convert.ToBase64String(
System.Text.Encoding.UTF8.GetBytes(rawJson)
);

// ==================================================================
// ШАГ 1: Создаём контейнер и записываем base64 чанками в атрибуты
// ==================================================================
int CHUNK = 50000;
int totalChunks = (int)Math.Ceiling((double)base64Json.Length / CHUNK);

// Создаём контейнер-div
instance.ActiveTab.MainDocument.EvaluateScript(
"var old=document.getElementById('zp_c');if(old)old.remove();" +
"var d=document.createElement('div');d.id='zp_c';d.style='display:none';" +
"d.setAttribute('data-n','" + totalChunks + "');" +
"document.body.appendChild(d);"
);
System.Threading.Thread.Sleep(200);

// Пишем каждый чанк в отдельный span внутри контейнера
for (int i = 0; i < totalChunks; i++)
{
int start = i * CHUNK;
int len = Math.Min(CHUNK, base64Json.Length - start);
string chunk = base64Json.Substring(start, len);

instance.ActiveTab.MainDocument.EvaluateScript(
"var c=document.getElementById('zp_c');" +
"if(c){var s=document.createElement('span');" +
"s.setAttribute('data-i','" + i + "');" +
"s.setAttribute('data-v','" + chunk + "');" +
"c.appendChild(s);}"
);
}
System.Threading.Thread.Sleep(300);

// Проверяем что контейнер создался
var containerEl = instance.ActiveTab.FindElementById("zp_c");
if (containerEl.IsVoid)
throw new Exception("Контейнер zp_c не найден после создания.");

string writtenChunks = containerEl.GetAttribute("data-n");
project.SendInfoToLog("Записано чанков (data-n): " + writtenChunks, true);

// ==================================================================
// ШАГ 2: JS собирает чанки из DOM, обрабатывает, пишет результат
// обратно в DOM атрибутами
// ==================================================================
string processJs =
"var zp_cont=document.getElementById('zp_c');" +
"if(zp_cont){" +
"var spans=zp_cont.getElementsByTagName('span');" +
"var b64='';" +
"for(var i=0;i<spans.length;i++){" +
" var s=spans[i]; var idx=parseInt(s.getAttribute('data-i'));"+
" b64+=s.getAttribute('data-v');" +
"}" +
"var raw=JSON.parse(atob(b64));" +
"var M=50,iIds={};" +
"raw.elements.forEach(function(e){if(e.interactive&&e.id)iIds[e.id]=1;});" +
"var flt=[];" +
"raw.elements.forEach(function(e){" +
" if(!e.visible||!e.w||!e.h)return;" +
" var tg=(e.tag||'').toLowerCase();" +
" if(e.interactive){flt.push(e);return;}" +
" if(iIds[e.parentId])return;" +
" if(e.isImage&&e.w>=16&&e.h>=16){flt.push(e);return;}" +
" if(e.isNav&&e.w>M&&e.h>M){flt.push(e);return;}" +
" if(tg==='h1'||tg==='h2'||tg==='h3'||tg==='h4'){flt.push(e);return;}" +
" if((tg==='div'||tg==='section'||tg==='main'||tg==='form')" +
" &&(e.depth||0)<=6&&e.w>=M&&e.h>=M){flt.push(e);return;}" +
"});" +
"function cs(s,n){if(!s)return '';s=String(s).replace(/\\s+/g,' ').trim();return s.length>n?s.substring(0,n)+'...':s;}" +
"var cl=flt.map(function(e){" +
" var o={id:e.id||0,tag:(e.tag||'').toLowerCase()," +
" x:e.x||0,y:e.y||0,w:e.w||0,h:e.h||0,cx:e.cx||0,cy:e.cy||0};" +
" var t=cs(e.text,150);if(t)o.text=t;" +
" var lb=e.ariaLabel||e.title||e.alt||'';if(lb)o.label=cs(lb,100);" +
" if(e.interactive)o.interactive=true;" +
" if(e.isImage)o.image=true;" +
" if(e.role)o.role=e.role;if(e.type)o.type=e.type;" +
" if(e.elId)o.elId=e.elId;" +
" if(e.cls)o.cls=String(e.cls).substring(0,80);" +
" if(e.href)o.href=String(e.href).substring(0,200);" +
" if(e.isImage&&e.src&&e.src.indexOf('data:')!==0)o.src=String(e.src).substring(0,200);" +
" if(e.value)o.value=cs(String(e.value),100);" +
" if(e.ph)o.placeholder=cs(e.ph,100);" +
" if(e.disabled)o.disabled=true;if(e.readOnly)o.readOnly=true;" +
" if(e.required)o.required=true;if(e.checked)o.checked=true;" +
" if(e.cursor==='pointer'&&!e.interactive)o.cursor='pointer';" +
" if(e.position)o.position=e.position;" +
" return o;" +
"});" +
"var m=raw.meta||{};" +
"var out=JSON.stringify({" +
" page:{url:m.url||'',title:m.title||''," +
" viewport:{w:m.viewportW||0,h:m.viewportH||0}," +
" pageSize:{w:m.pageW||0,h:m.pageH||0}," +
" scroll:{x:m.scrollX||0,y:m.scrollY||0}}," +
" stats:{totalRaw:raw.elements.length,totalFiltered:cl.length," +
" reduction:Math.round(100-100*cl.length/Math.max(raw.elements.length,1))+'%'}," +
" elements:cl" +
"});" +
// Кодируем результат в base64 чтобы избежать порчи Unicode в DOM-атрибутах
"var RCHUNK=50000;" +
"var rChunks=Math.ceil(out.length/RCHUNK);" +
"var old2=document.getElementById('zp_r');if(old2)old2.remove();" +
"var rd=document.createElement('div');rd.id='zp_r';rd.style='display:none';" +
"rd.setAttribute('data-n',String(rChunks));" +
"rd.setAttribute('data-f',String(cl.length));" +
"rd.setAttribute('data-t',String(raw.elements.length));" +
"document.body.appendChild(rd);" +
"for(var j=0;j<rChunks;j++){" +
" var rs=document.createElement('span');" +
" rs.setAttribute('data-i',String(j));" +
" rs.setAttribute('data-v',out.substring(j*RCHUNK,(j+1)*RCHUNK));" +
" rd.appendChild(rs);" +
"}" +
// Удаляем входной контейнер
"zp_cont.remove();" +
"}";

instance.ActiveTab.MainDocument.EvaluateScript(processJs);
System.Threading.Thread.Sleep(500);

// ==================================================================
// ШАГ 3: Читаем результат из DOM
// ==================================================================
var resultDiv = instance.ActiveTab.FindElementById("zp_r");
int wc = 0;
while (resultDiv.IsVoid && wc < 40)
{
System.Threading.Thread.Sleep(100);
resultDiv = instance.ActiveTab.FindElementById("zp_r");
wc++;
}
if (resultDiv.IsVoid)
throw new Exception("Результирующий div zp_r не найден — JS не выполнился или данные не записались.");

string nChunksStr = resultDiv.GetAttribute("data-n");
string nFilt = resultDiv.GetAttribute("data-f");
string nTotal = resultDiv.GetAttribute("data-t");

project.SendInfoToLog($"Результат: filtered={nFilt}, total={nTotal}, chunks={nChunksStr}", true);

int nChunks = 0;
int.TryParse(nChunksStr, out nChunks);
if (nChunks == 0)
throw new Exception("data-n = 0 или пустой.");

// Читаем чанки результата через дочерние span
var sb = new System.Text.StringBuilder();
for (int i = 0; i < nChunks; i++)
{
// Ищем span с data-i=i внутри zp_r через XPath
var chunkSpan = instance.ActiveTab.FindElementByXPath(
"//div[@id='zp_r']/span[@data-i='" + i + "']", 0
);
if (chunkSpan.IsVoid)
throw new Exception("Чанк span data-i=" + i + " не найден.");

string chunkVal = chunkSpan.GetAttribute("data-v");
if (chunkVal == null) chunkVal = "";
sb.Append(chunkVal);
}

// Убираем результирующий div
instance.ActiveTab.MainDocument.EvaluateScript(
"var e=document.getElementById('zp_r');if(e)e.remove();"
);

string rawResult = sb.ToString();
if (string.IsNullOrWhiteSpace(rawResult))
throw new Exception("Итоговый JSON пустой.");

// GetAttribute в ZP возвращает UTF-8 байты как Latin-1 символы.
// Исправляем: берём каждый char как байт и декодируем как UTF-8.
byte[] utf8bytes = new byte[rawResult.Length];
for (int k = 0; k < rawResult.Length; k++)
utf8bytes[k] = (byte)(rawResult[k] & 0xFF);
string finalJson = System.Text.Encoding.UTF8.GetString(utf8bytes);

if (string.IsNullOrWhiteSpace(finalJson))
throw new Exception("Итоговый JSON пустой.");

project.Variables["AiDomForAI"].Value = finalJson;

project.SendInfoToLog(
$"✅ Препроцессор v8 готов! {nTotal} → {nFilt} эл. | {finalJson.Length} символов",
true
);

return finalJson;
}
catch (Exception ex)
{
// Чистим DOM при ошибке
try {
instance.ActiveTab.MainDocument.EvaluateScript(
"['zp_c','zp_r'].forEach(function(id){var e=document.getElementById(id);if(e)e.remove();});"
);
} catch {}

project.SendErrorToLog($"❌ Ошибка: {ex.Message}", true);
return null;
}
Snippet A / Snippet A-Refresh — формирование промпта для LCM и CoordRefresh
Snippet A:
// =====================================================================
// SNIPPET A — LCM Prompt Builder v4 (точное соответствие Python LCM)
// Читает DOM, task.txt, context_store.json → формирует AiPrompt
// DOM предобрабатывается как в Python: только interactive элементы,
// cx/cy → x/y, фильтрация мусора (CSS, невидимые, disabled)
// =====================================================================

string BOT_DIR = project.Directory + @"\";

// ── Читаем задачу ─────────────────────────────────────────────────────
string task = "";
try {
string taskFile = BOT_DIR + "task.txt";
if (System.IO.File.Exists(taskFile))
task = System.IO.File.ReadAllText(taskFile, System.Text.Encoding.UTF8).Trim();
} catch (System.Exception ex) {
project.SendErrorToLog("Ошибка чтения task.txt: " + ex.Message, true);
}
if (string.IsNullOrEmpty(task)) {
project.SendErrorToLog("task.txt пустой или не найден!", true);
return "ERROR_NO_TASK";
}

// ── Читаем DOM ────────────────────────────────────────────────────────
string domRaw = "";
try {
string domFile = BOT_DIR + "dom_input.txt";
if (!System.IO.File.Exists(domFile)) domFile = BOT_DIR + "lcm_ai_module.txt";
if (System.IO.File.Exists(domFile))
domRaw = System.IO.File.ReadAllText(domFile, System.Text.Encoding.UTF8).Trim();
} catch (System.Exception ex) {
project.SendErrorToLog("Ошибка чтения DOM: " + ex.Message, true);
}
if (string.IsNullOrEmpty(domRaw)) {
project.SendErrorToLog("DOM файл не найден!", true);
return "ERROR_NO_DOM";
}

// ── Предобработка DOM — точно как в Python _prepare_dom() ────────────
// Фильтруем: только interactive элементы с реальными координатами
// Нормализуем: cx/cy → x/y (центр элемента)
// Убираем: невидимые (w=0,h=0), CSS-мусор, disabled
string domCleaned = domRaw;
try {
var domObj = Global.ZennoLab.Json.Linq.JToken.Parse(domRaw);

// Вытаскиваем массив elements из любой структуры
Global.ZennoLab.Json.Linq.JArray rawElements = null;
if (domObj is Global.ZennoLab.Json.Linq.JArray) {
rawElements = (Global.ZennoLab.Json.Linq.JArray)domObj;
} else if (domObj is Global.ZennoLab.Json.Linq.JObject) {
var domJObj = (Global.ZennoLab.Json.Linq.JObject)domObj;
foreach (var key in new string[] { "elements", "buttons", "items", "nodes" }) {
var token = domJObj[key];
if (token is Global.ZennoLab.Json.Linq.JArray) {
rawElements = (Global.ZennoLab.Json.Linq.JArray)token;
break;
}
}
}

if (rawElements != null && rawElements.Count > 0) {
var cleaned = new Global.ZennoLab.Json.Linq.JArray();

foreach (var el in rawElements) {
if (!(el is Global.ZennoLab.Json.Linq.JObject)) continue;
var e = (Global.ZennoLab.Json.Linq.JObject)el;

// Пропускаем невидимые (w=0 и h=0)
int w = 0, h = 0;
try { int.TryParse(e["w"] != null ? e["w"].ToString() : "0", out w); } catch { }
try { int.TryParse(e["h"] != null ? e["h"].ToString() : "0", out h); } catch { }
if (w == 0 && h == 0) continue;

// Пропускаем disabled
string disabledVal = e["disabled"] != null ? e["disabled"].ToString().ToLower() : "";
if (disabledVal == "true") continue;

// Пропускаем не-интерактивные (у которых нет interactive=true)
string interactiveVal = e["interactive"] != null ? e["interactive"].ToString().ToLower() : "";
if (interactiveVal != "true") continue;

// Нормализуем координаты: приоритет cx/cy (центр), fallback x/y
int cx = 0, cy = 0, x = 0, y = 0;
try { int.TryParse(e["cx"] != null ? e["cx"].ToString() : "0", out cx); } catch { }
try { int.TryParse(e["cy"] != null ? e["cy"].ToString() : "0", out cy); } catch { }
try { int.TryParse(e["x"] != null ? e["x"].ToString() : "0", out x); } catch { }
try { int.TryParse(e["y"] != null ? e["y"].ToString() : "0", out y); } catch { }
int finalX = cx > 0 ? cx : x;
int finalY = cy > 0 ? cy : y;
if (finalX == 0 && finalY == 0) continue; // координаты нулевые — бесполезно

// Собираем label: приоритет label → text → aria-label → placeholder → id
string label = "";
foreach (var lk in new string[] { "label", "text", "aria-label", "placeholder", "title", "name", "id" }) {
string lv = e[lk] != null ? e[lk].ToString().Trim() : "";
// Пропускаем CSS-мусор (длинные строки с { или .className)
if (lv.Length > 0 && lv.Length < 120 && !lv.Contains("{") && !lv.Contains("display:")) {
label = lv;
break;
}
}
if (string.IsNullOrEmpty(label)) continue; // элемент без метки бесполезен

// Строим очищенный элемент
var cleanEl = new Global.ZennoLab.Json.Linq.JObject();
cleanEl["tag"] = e["tag"] != null ? e["tag"].ToString().ToLower() : "?";
cleanEl["label"] = label.Length > 80 ? label.Substring(0, 80) : label;
cleanEl["x"] = finalX;
cleanEl["y"] = finalY;
cleanEl["w"] = w;
cleanEl["h"] = h;

// Дополнительные поля если есть
foreach (var extraKey in new string[] { "href", "type", "role", "value" }) {
string ev = e[extraKey] != null ? e[extraKey].ToString() : "";
if (!string.IsNullOrEmpty(ev) && ev.Length < 80)
cleanEl[extraKey] = ev;
}

cleaned.Add(cleanEl);
}

if (cleaned.Count > 0) {
domCleaned = cleaned.ToString(Global.ZennoLab.Json.Formatting.Indented);
project.SendInfoToLog("DOM: " + rawElements.Count + " raw → " + cleaned.Count + " интерактивных элементов", true);
} else {
// Если после фильтрации ничего не осталось — берём исходный DOM
project.SendInfoToLog("DOM: нет интерактивных элементов после фильтрации — используем raw", false);
if (domRaw.Length > 18000) domCleaned = domRaw.Substring(0, 18000) + "\n... [truncated]";
}
}
} catch (System.Exception ex) {
project.SendInfoToLog("DOM preprocessing err (используем raw): " + ex.Message, false);
if (domRaw.Length > 18000) domCleaned = domRaw.Substring(0, 18000) + "\n... [truncated]";
}

// ── Читаем контекст ───────────────────────────────────────────────────
string contextSummary = "Context is empty — no previous state.";
try {
string contextFile = BOT_DIR + "context_store.json";
if (System.IO.File.Exists(contextFile)) {
string ctxRaw = System.IO.File.ReadAllText(contextFile, System.Text.Encoding.UTF8);
var ctxObj = Global.ZennoLab.Json.Linq.JObject.Parse(ctxRaw);
var ctxLines = new System.Collections.Generic.List<string>();
foreach (var prop in ctxObj.Properties()) {
string val = "?"; string ts = "";
try { val = prop.Value["value"] != null ? prop.Value["value"].ToString() : "?"; } catch { }
try { ts = prop.Value["saved_at"] != null ? prop.Value["saved_at"].ToString() : ""; } catch { }
string ln = " " + prop.Name + ": " + val;
if (ts.Length > 0) ln += " (saved at " + ts + ")";
ctxLines.Add(ln);
}
if (ctxLines.Count > 0)
contextSummary = string.Join("\n", ctxLines.ToArray());
}
} catch (System.Exception ex) {
project.SendInfoToLog("context_store read err: " + ex.Message, false);
}

// ── URL / Title ───────────────────────────────────────────────────────
string curUrl = ""; try { curUrl = instance.ActiveTab.URL; } catch { }
string curTitle = ""; try { curTitle = instance.ActiveTab.Title; } catch { }

// ── SYSTEM PROMPT — точная копия из Python LCM ────────────────────────
string systemPrompt =
@"You are a precise web automation planner for ZennoPoster.
Given a JSON list of UI elements visible on a web page, a task, and any saved context,
plan a COMPLETE step-by-step action chain to accomplish the task.

CRITICAL INSTRUCTION: Your ENTIRE response must be a single valid JSON object.
Start your response with { and end with }.
Do NOT write any text before or after the JSON.
Do NOT use markdown code blocks or ```json fences.
Do NOT explain anything. Output raw JSON only.

CRITICAL: YOU MUST USE THE EXACT JSON STRUCTURE BELOW. DO NOT invent your own keys.
You MUST extract the exact ""x"" and ""y"" coordinates and ""label"" from the provided DOM elements.
NEVER use CSS selectors, XPath, or ""#id"" references as values for ""label"", ""x"", or ""y"".

EXAMPLE OF EXPECTED OUTPUT:
{
""task_summary"": ""Click the new chat button and wait"",
""confidence"": ""high"",
""total_steps"": 1,
""steps"": [
{
""step_num"": 1,
""action"": ""CLICK"",
""label"": ""New chat"",
""x"": 12,
""y"": 80,
""value"": """",
""scroll_dir"": ""down"",
""scroll_px"": 300,
""wait_sec"": 2,
""reason"": ""Clicking the New chat button"",
""save_key"": """",
""save_from"": ""none"",
""save_hint"": """"
}
]
}

Response schema rules:
- ""action"": strictly ""CLICK"", ""INPUT"", ""SCROLL"", ""NAVIGATE"", ""WAIT"", or ""DONE"". NEVER ""click"", ""type"", ""press"", ""fill"".
- ""x"", ""y"": MANDATORY integers copied EXACTLY from the matching DOM element. NEVER use 0. NEVER use CSS selectors.
- ""label"": plain text name of the element as it appears on screen.
- ""reason"": one short sentence explaining WHY this step is needed.

Planning rules:
1. ONLY generate steps STRICTLY REQUIRED to complete the TASK. A typical task takes 1-3 steps.
2. The ""action"" field MUST BE EXACTLY ONE OF the allowed values (UPPERCASE).
3. Use the exact ""x"" and ""y"" coordinates from the provided DOM elements.
4. After any CLICK that opens a new page or dialog, add a WAIT step (wait_sec=2).
5. For text input: first CLICK the field, then INPUT the value.
6. ""save_key"" naming: use snake_case. ""save_from"" values: ""url"", ""page_title"", ""input_value"", ""none"".
7. If the TASK says the browser is already on page — do NOT navigate. Click ONLY the specific element named.
8. If the target element is NOT present in DOM, output a single DONE step with reason ""element not found in current DOM"".";

// ── USER PROMPT — точная структура как в Python _build_prompt() ────────
var userSb = new System.Text.StringBuilder();
userSb.AppendLine("TASK:");
userSb.AppendLine(task);
userSb.AppendLine();
userSb.AppendLine("SAVED CONTEXT (state from previous runs):");
userSb.AppendLine(contextSummary);
userSb.AppendLine();
userSb.AppendLine("CURRENT PAGE:");
userSb.AppendLine("URL: " + curUrl);
userSb.AppendLine("Title: " + curTitle);
userSb.AppendLine();
userSb.AppendLine("AVAILABLE DOM UI ELEMENTS (x/y are center coordinates for clicking):");
userSb.AppendLine(domCleaned);
userSb.AppendLine();
userSb.AppendLine("CRITICAL INSTRUCTION: Plan ONLY the specific steps needed to accomplish the TASK: '" + task + "'.");
userSb.AppendLine("Do NOT list all available elements. Keep the chain as short as possible. Output ONLY valid JSON.");

// ── Финальный промпт = system + user (для чат-ИИ который не имеет system role) ──
var finalSb = new System.Text.StringBuilder();
finalSb.AppendLine(systemPrompt);
finalSb.AppendLine();
finalSb.AppendLine("---");
finalSb.AppendLine();
finalSb.Append(userSb.ToString());
string finalPrompt = finalSb.ToString().Trim();

// ── Сохраняем в файл (основной канал) ────────────────────────────────
try {
System.IO.File.WriteAllText(BOT_DIR + "ai_prompt.txt", finalPrompt,
new System.Text.UTF8Encoding(false));
} catch (System.Exception ex) {
project.SendErrorToLog("Ошибка записи ai_prompt.txt: " + ex.Message, true);
return "ERROR_FILE_WRITE";
}

// ── Пробуем записать в переменную (если создана в проекте) ────────────
try { project.Variables["AiPrompt"].Value = finalPrompt; } catch { }

project.SendInfoToLog(
"Snippet A OK | " + finalPrompt.Length + " символов | Задача: " +
task.Substring(0, System.Math.Min(task.Length, 80)), true);

return "SUCCESS";
Snippet A-Refresh:
// =====================================================================
// SNIPPET A-REFRESH — CoordRefresh Prompt Builder
// Замена logic_control_module_refresh.py
// Запускается когда CoordRefreshNeeded=true (после Snippet 2)
// Читает CoordRefreshPrompt + lcm_ai_module_after.txt (свежий DOM)
// → формирует AiPrompt для обновления координат оставшихся шагов
// =====================================================================

string BOT_DIR = project.Directory + @"\";

// ── Читаем prompt из переменной CoordRefreshPrompt ───────────────────
string coordPrompt = "";
try { coordPrompt = project.Variables["CoordRefreshPrompt"].Value.Trim(); } catch { }

// Fallback: читаем из файла если переменная пуста
if (string.IsNullOrEmpty(coordPrompt)) {
try {
string cpFile = BOT_DIR + "coord_refresh_prompt.txt";
if (System.IO.File.Exists(cpFile))
coordPrompt = System.IO.File.ReadAllText(cpFile, System.Text.Encoding.UTF8).Trim();
} catch { }
}

if (string.IsNullOrEmpty(coordPrompt)) {
project.SendErrorToLog("CoordRefreshPrompt пустой! Snippet 2 не записал его.", true);
return "ERROR_NO_COORD_PROMPT";
}

// ── Читаем СВЕЖИЙ DOM из lcm_ai_module_after.txt (снят ПОСЛЕ действия) ─
// Если нет after — используем текущий dom_input.txt
string domRaw = "";
bool usedAfter = false;
try {
string afterDomFile = BOT_DIR + "lcm_ai_module_after.txt";
string domInputFile = BOT_DIR + "dom_input.txt";
string fallbackFile = BOT_DIR + "lcm_ai_module.txt";

if (System.IO.File.Exists(afterDomFile)) {
domRaw = System.IO.File.ReadAllText(afterDomFile, System.Text.Encoding.UTF8).Trim();
usedAfter = true;
} else if (System.IO.File.Exists(domInputFile)) {
domRaw = System.IO.File.ReadAllText(domInputFile, System.Text.Encoding.UTF8).Trim();
} else if (System.IO.File.Exists(fallbackFile)) {
domRaw = System.IO.File.ReadAllText(fallbackFile, System.Text.Encoding.UTF8).Trim();
}
} catch (System.Exception ex) {
project.SendInfoToLog("DOM read err (refresh): " + ex.Message, false);
}

if (string.IsNullOrEmpty(domRaw)) {
project.SendErrorToLog("DOM файл не найден для CoordRefresh!", true);
return "ERROR_NO_DOM";
}

// ── Предобработка DOM — только interactive элементы ──────────────────
string domCleaned = domRaw;
try {
var domObj = Global.ZennoLab.Json.Linq.JToken.Parse(domRaw);
Global.ZennoLab.Json.Linq.JArray rawElements = null;

if (domObj is Global.ZennoLab.Json.Linq.JArray) {
rawElements = (Global.ZennoLab.Json.Linq.JArray)domObj;
} else if (domObj is Global.ZennoLab.Json.Linq.JObject) {
var domJObj = (Global.ZennoLab.Json.Linq.JObject)domObj;
foreach (var key in new string[] { "elements", "buttons", "items", "nodes" }) {
var token = domJObj[key];
if (token is Global.ZennoLab.Json.Linq.JArray) {
rawElements = (Global.ZennoLab.Json.Linq.JArray)token;
break;
}
}
}

if (rawElements != null && rawElements.Count > 0) {
var cleaned = new Global.ZennoLab.Json.Linq.JArray();
foreach (var el in rawElements) {
if (!(el is Global.ZennoLab.Json.Linq.JObject)) continue;
var e = (Global.ZennoLab.Json.Linq.JObject)el;

int w = 0, h = 0;
try { int.TryParse(e["w"] != null ? e["w"].ToString() : "0", out w); } catch { }
try { int.TryParse(e["h"] != null ? e["h"].ToString() : "0", out h); } catch { }
if (w == 0 && h == 0) continue;

string disabledVal = e["disabled"] != null ? e["disabled"].ToString().ToLower() : "";
if (disabledVal == "true") continue;

string interactiveVal = e["interactive"] != null ? e["interactive"].ToString().ToLower() : "";
if (interactiveVal != "true") continue;

int cx = 0, cy = 0, x = 0, y = 0;
try { int.TryParse(e["cx"] != null ? e["cx"].ToString() : "0", out cx); } catch { }
try { int.TryParse(e["cy"] != null ? e["cy"].ToString() : "0", out cy); } catch { }
try { int.TryParse(e["x"] != null ? e["x"].ToString() : "0", out x); } catch { }
try { int.TryParse(e["y"] != null ? e["y"].ToString() : "0", out y); } catch { }
int finalX = cx > 0 ? cx : x;
int finalY = cy > 0 ? cy : y;
if (finalX == 0 && finalY == 0) continue;

string label = "";
foreach (var lk in new string[] { "label", "text", "aria-label", "placeholder", "title", "name", "id" }) {
string lv = e[lk] != null ? e[lk].ToString().Trim() : "";
if (lv.Length > 0 && lv.Length < 120 && !lv.Contains("{") && !lv.Contains("display:")) {
label = lv;
break;
}
}
if (string.IsNullOrEmpty(label)) continue;

var cleanEl = new Global.ZennoLab.Json.Linq.JObject();
cleanEl["tag"] = e["tag"] != null ? e["tag"].ToString().ToLower() : "?";
cleanEl["label"] = label.Length > 80 ? label.Substring(0, 80) : label;
cleanEl["x"] = finalX;
cleanEl["y"] = finalY;
cleanEl["w"] = w;
cleanEl["h"] = h;
foreach (var extraKey in new string[] { "href", "type", "role", "value" }) {
string ev = e[extraKey] != null ? e[extraKey].ToString() : "";
if (!string.IsNullOrEmpty(ev) && ev.Length < 80) cleanEl[extraKey] = ev;
}
cleaned.Add(cleanEl);
}

if (cleaned.Count > 0) {
domCleaned = cleaned.ToString(Global.ZennoLab.Json.Formatting.Indented);
project.SendInfoToLog(
"DOM (refresh): " + rawElements.Count + " raw → " + cleaned.Count +
" интерактивных" + (usedAfter ? " [from after.txt]" : " [from dom_input]"), true);
}
}
} catch (System.Exception ex) {
project.SendInfoToLog("DOM preprocessing err (refresh): " + ex.Message, false);
if (domRaw.Length > 18000) domCleaned = domRaw.Substring(0, 18000) + "\n... [truncated]";
}

// ── URL / Title ───────────────────────────────────────────────────────
string curUrl = ""; try { curUrl = instance.ActiveTab.URL; } catch { }
string curTitle = ""; try { curTitle = instance.ActiveTab.Title; } catch { }

// ── SYSTEM PROMPT для COORD-REFRESH режима ────────────────────────────
string systemPrompt =
@"You are a precise web automation planner for ZennoPoster.
Your task is COORD-REFRESH: update the x,y coordinates of remaining steps using the current DOM.

CRITICAL INSTRUCTION: Your ENTIRE response must be a single valid JSON object.
Start your response with { and end with }.
Do NOT write any text before or after the JSON.
Do NOT use markdown code blocks or ```json fences.
Do NOT explain anything. Output raw JSON only.

CRITICAL: YOU MUST USE THE EXACT JSON STRUCTURE BELOW.
NEVER use CSS selectors, XPath, or ""#id"" references.

EXAMPLE OF EXPECTED OUTPUT:
{
""task_summary"": ""Updated coordinates for remaining steps"",
""confidence"": ""high"",
""total_steps"": 2,
""steps"": [
{
""step_num"": 1,
""action"": ""INPUT"",
""label"": ""Project Name"",
""x"": 744,
""y"": 382,
""value"": ""my project"",
""scroll_dir"": ""down"",
""scroll_px"": 300,
""wait_sec"": 2,
""reason"": ""Entering project name into the input field"",
""save_key"": """",
""save_from"": ""none"",
""save_hint"": """"
}
]
}

COORD-REFRESH rules:
1. Keep action, label, value EXACTLY as specified in the task — do NOT change them.
2. Find the matching DOM element by label/text and copy its FRESH x,y coordinates.
3. step_num starts from 1 regardless of original numbering.
4. If a step element is not found in DOM — keep original coordinates but add reason ""element not found, using original coords"".
5. Output ALL listed remaining steps, not just the ones you updated.
6. ""action"" field MUST BE EXACTLY ONE OF: ""CLICK"", ""INPUT"", ""SCROLL"", ""NAVIGATE"", ""WAIT"", ""DONE"".";

// ── USER PROMPT ────────────────────────────────────────────────────────
var userSb = new System.Text.StringBuilder();
userSb.AppendLine("COORD-REFRESH REQUEST:");
userSb.AppendLine(coordPrompt);
userSb.AppendLine();
userSb.AppendLine("CURRENT PAGE:");
userSb.AppendLine("URL: " + curUrl);
userSb.AppendLine("Title: " + curTitle);
userSb.AppendLine();
userSb.AppendLine("CURRENT DOM ELEMENTS (use these x/y coordinates — they are FRESH):");
userSb.AppendLine(domCleaned);
userSb.AppendLine();
userSb.AppendLine("Return updated action_chain JSON with FRESH x,y for ALL listed steps.");
userSb.AppendLine("Keep action/label/value unchanged. Output ONLY valid JSON.");

// ── Финальный промпт ──────────────────────────────────────────────────
var finalSb = new System.Text.StringBuilder();
finalSb.AppendLine(systemPrompt);
finalSb.AppendLine();
finalSb.AppendLine("---");
finalSb.AppendLine();
finalSb.Append(userSb.ToString());
string finalPrompt = finalSb.ToString().Trim();

// ── Сохраняем ─────────────────────────────────────────────────────────
try {
System.IO.File.WriteAllText(BOT_DIR + "ai_prompt.txt", finalPrompt,
new System.Text.UTF8Encoding(false));
} catch (System.Exception ex) {
project.SendErrorToLog("Ошибка записи ai_prompt.txt (refresh): " + ex.Message, true);
return "ERROR_FILE_WRITE";
}
try { project.Variables["AiPrompt"].Value = finalPrompt; } catch { }

project.SendInfoToLog(
"Snippet A-Refresh OK | " + finalPrompt.Length + " символов | CoordRefresh", true);

return "SUCCESS";
Snippet B — парсинг action_chain.json из ответа ИИ
Snippet B:
// =====================================================================
// SNIPPET B — AI Response Parser v2 (точное соответствие Python LCM output)
// Читает AiResponse / ai_response.txt → парсит JSON →
// пишет action_chain.json + next_action.txt
//
// Отличия от v1:
// - Поддержка поля "reason" в каждом шаге
// - Нормализация action aliases (type→INPUT, press→CLICK, go→NAVIGATE и т.д.)
// - Фиксация нулевых координат через поиск по label в DOM
// - Чтение ответа из файла ai_response.txt если переменная пуста
// - action_chain.json содержит "task" + "task_summary" (как в Python)
// =====================================================================

string BOT_DIR = project.Directory + @"\";
string CHAIN_FILE = BOT_DIR + "action_chain.json";
string NEXT_FILE = BOT_DIR + "next_action.txt";

// ── Читаем ответ ИИ ───────────────────────────────────────────────────
string aiRaw = "";
try { aiRaw = project.Variables["AiResponse"].Value.Trim(); } catch { }

// Fallback: читаем из файла если переменная пуста или не создана
if (string.IsNullOrEmpty(aiRaw)) {
try {
string respFile = BOT_DIR + "ai_response.txt";
if (System.IO.File.Exists(respFile))
aiRaw = System.IO.File.ReadAllText(respFile, System.Text.Encoding.UTF8).Trim();
} catch { }
}

if (string.IsNullOrEmpty(aiRaw)) {
project.SendErrorToLog("AiResponse пустой! Переменная не заполнена и файл ai_response.txt не найден.", true);
return "ERROR_EMPTY_AI_RESPONSE";
}

// ── Извлекаем JSON ────────────────────────────────────────────────────
// Метод: ищем первый корректно закрытый { ... } блок (как в Python _parse_chain)
string jsonStr = aiRaw;

// Убираем markdown-обёртки
int backtickPos = jsonStr.IndexOf("```");
if (backtickPos >= 0) {
int nlPos = jsonStr.IndexOf('\n', backtickPos);
int lastTick = jsonStr.LastIndexOf("```");
if (nlPos >= 0 && lastTick > nlPos)
jsonStr = jsonStr.Substring(nlPos + 1, lastTick - nlPos - 1).Trim();
}

// Ищем первый сбалансированный { ... } блок
string extractedJson = "";
for (int si = 0; si < jsonStr.Length; si++) {
if (jsonStr[si] != '{') continue;
int depth = 0; int endIdx = -1;
for (int ei = si; ei < jsonStr.Length; ei++) {
if (jsonStr[ei] == '{') depth++;
else if (jsonStr[ei] == '}') { depth--; if (depth == 0) { endIdx = ei; break; } }
}
if (endIdx > si) { extractedJson = jsonStr.Substring(si, endIdx - si + 1); break; }
}

if (string.IsNullOrEmpty(extractedJson)) {
project.SendErrorToLog("Не удалось извлечь JSON из ответа ИИ:\n" +
aiRaw.Substring(0, System.Math.Min(aiRaw.Length, 400)), true);
return "ERROR_NO_JSON_IN_RESPONSE";
}

// ── Парсим JSON ───────────────────────────────────────────────────────
Global.ZennoLab.Json.Linq.JObject jObj = null;
Global.ZennoLab.Json.Linq.JArray stepsRaw = null;

try {
jObj = Global.ZennoLab.Json.Linq.JObject.Parse(extractedJson);
} catch (System.Exception ex) {
project.SendErrorToLog("JSON parse error: " + ex.Message +
"\nRaw:\n" + extractedJson.Substring(0, System.Math.Min(extractedJson.Length, 400)), true);
return "ERROR_JSON_PARSE";
}

// Пробуем все возможные структуры (как Python: steps, actions, task.steps)
stepsRaw = jObj["steps"] as Global.ZennoLab.Json.Linq.JArray;
if (stepsRaw == null) stepsRaw = jObj["actions"] as Global.ZennoLab.Json.Linq.JArray;
if (stepsRaw == null && jObj["task"] is Global.ZennoLab.Json.Linq.JObject) {
var taskObj = (Global.ZennoLab.Json.Linq.JObject)jObj["task"];
stepsRaw = taskObj["steps"] as Global.ZennoLab.Json.Linq.JArray;
if (stepsRaw == null) stepsRaw = taskObj["actions"] as Global.ZennoLab.Json.Linq.JArray;
}

if (stepsRaw == null || stepsRaw.Count == 0) {
project.SendErrorToLog("В ответе ИИ нет массива steps или он пустой! Keys: " + extractedJson.Substring(0, 200), true);
return "ERROR_EMPTY_STEPS";
}

// ── Нормализация action aliases (как в Python ACT_ALIASES) ────────────
System.Func<string, string> normalizeAction = (act) => {
string a = act.Trim().ToUpper();
if (a == "TYPE" || a == "FILL" || a == "ENTER") return "INPUT";
if (a == "PRESS" || a == "TAP") return "CLICK";
if (a == "GO" || a == "GOTO") return "NAVIGATE";
if (a == "SLEEP" || a == "PAUSE") return "WAIT";
// Принимаем только разрешённые значения
if (a == "CLICK" || a == "INPUT" || a == "SCROLL" ||
a == "NAVIGATE" || a == "WAIT" || a == "DONE" || a == "DOM_RESCAN") return a;
return "ERROR";
};

// ── Читаем DOM для фиксации нулевых координат (fallback) ─────────────
var domFallbackMap = new System.Collections.Generic.Dictionary<string, int[]>(
System.StringComparer.OrdinalIgnoreCase);
try {
string domFile = BOT_DIR + "dom_input.txt";
if (!System.IO.File.Exists(domFile)) domFile = BOT_DIR + "lcm_ai_module.txt";
if (System.IO.File.Exists(domFile)) {
string domRaw = System.IO.File.ReadAllText(domFile, System.Text.Encoding.UTF8);
var domObj = Global.ZennoLab.Json.Linq.JToken.Parse(domRaw);
Global.ZennoLab.Json.Linq.JArray domEls = null;
if (domObj is Global.ZennoLab.Json.Linq.JArray) {
domEls = (Global.ZennoLab.Json.Linq.JArray)domObj;
} else if (domObj is Global.ZennoLab.Json.Linq.JObject) {
var dj = (Global.ZennoLab.Json.Linq.JObject)domObj;
foreach (var k in new string[] { "elements", "buttons", "items" }) {
if (dj[k] is Global.ZennoLab.Json.Linq.JArray) { domEls = (Global.ZennoLab.Json.Linq.JArray)dj[k]; break; }
}
}
if (domEls != null) {
foreach (var el in domEls) {
if (!(el is Global.ZennoLab.Json.Linq.JObject)) continue;
var e = (Global.ZennoLab.Json.Linq.JObject)el;
int cx = 0, cy = 0;
try { int.TryParse(e["cx"] != null ? e["cx"].ToString() : "0", out cx); } catch { }
try { int.TryParse(e["cy"] != null ? e["cy"].ToString() : "0", out cy); } catch { }
if (cx == 0) try { int.TryParse(e["x"] != null ? e["x"].ToString() : "0", out cx); } catch { }
if (cy == 0) try { int.TryParse(e["y"] != null ? e["y"].ToString() : "0", out cy); } catch { }
if (cx == 0 && cy == 0) continue;
string lbl = "";
foreach (var lk in new string[] { "label", "text", "aria-label", "placeholder" }) {
string lv = e[lk] != null ? e[lk].ToString().Trim() : "";
if (lv.Length > 0 && lv.Length < 120 && !lv.Contains("{")) { lbl = lv.ToLower(); break; }
}
if (!string.IsNullOrEmpty(lbl) && !domFallbackMap.ContainsKey(lbl))
domFallbackMap[lbl] = new int[] { cx, cy };
}
}
}
} catch { }

// ── Обрабатываем шаги ────────────────────────────────────────────────
var parsedSteps = new System.Collections.Generic.List<Global.ZennoLab.Json.Linq.JObject>();

foreach (var s in stepsRaw) {
if (!(s is Global.ZennoLab.Json.Linq.JObject)) continue;
var step = (Global.ZennoLab.Json.Linq.JObject)s;
System.Func<string, string, string> gs = (k, d) =>
step[k] != null ? step[k].ToString() : d;

string action = normalizeAction(gs("action", "ERROR"));

// Координаты: x/y или cx/cy
int x = 0, y = 0;
try { int.TryParse(gs("x", "0"), out x); } catch { }
try { int.TryParse(gs("y", "0"), out y); } catch { }
if (x == 0) try { int.TryParse(gs("cx", "0"), out x); } catch { }
if (y == 0) try { int.TryParse(gs("cy", "0"), out y); } catch { }

string label = gs("label", "");
// Также проверяем target/element/selector как в Python
if (string.IsNullOrEmpty(label)) {
var targetToken = step["target"] ?? step["element"] ?? step["selector"];
if (targetToken != null) {
if (targetToken is Global.ZennoLab.Json.Linq.JObject) {
var tObj = (Global.ZennoLab.Json.Linq.JObject)targetToken;
label = tObj["label"] != null ? tObj["label"].ToString() :
(tObj["text"] != null ? tObj["text"].ToString() : "");
if (x == 0) try { int.TryParse(tObj["x"] != null ? tObj["x"].ToString() : "0", out x); } catch { }
if (y == 0) try { int.TryParse(tObj["y"] != null ? tObj["y"].ToString() : "0", out y); } catch { }
} else {
label = targetToken.ToString();
}
}
}

// Фиксируем нулевые координаты через DOM fallback (как Python _fix_zero_coords)
if ((x == 0 || y == 0) && !string.IsNullOrEmpty(label)) {
string labelLow = label.ToLower().Trim();
if (domFallbackMap.ContainsKey(labelLow)) {
x = domFallbackMap[labelLow][0];
y = domFallbackMap[labelLow][1];
project.SendInfoToLog("Coord fallback: '" + label + "' → (" + x + "," + y + ")", false);
} else {
// Частичное совпадение
foreach (var kv in domFallbackMap) {
if (kv.Key.Contains(labelLow) || labelLow.Contains(kv.Key)) {
x = kv.Value[0]; y = kv.Value[1];
project.SendInfoToLog("Coord fallback (partial): '" + label + "' ~ '" + kv.Key + "' → (" + x + "," + y + ")", false);
break;
}
}
}
}

// wait_sec: проверяем несколько вариантов ключей
int waitSec = 2;
try { int.TryParse(gs("wait_sec", "2"), out waitSec); } catch { }
if (waitSec == 2) try { int.TryParse(gs("wait", "2"), out waitSec); } catch { }
if (waitSec == 2) try { int.TryParse(gs("time", "2"), out waitSec); } catch { }

int scrollPx = 300;
try { int.TryParse(gs("scroll_px", "300"), out scrollPx); } catch { }

int stepNum = parsedSteps.Count + 1;
try { int.TryParse(gs("step_num", stepNum.ToString()), out stepNum); } catch { }

var cleanStep = new Global.ZennoLab.Json.Linq.JObject();
cleanStep["step_num"] = stepNum;
cleanStep["action"] = action;
cleanStep["label"] = label;
cleanStep["x"] = x;
cleanStep["y"] = y;
cleanStep["value"] = gs("value", "");
cleanStep["scroll_dir"] = gs("scroll_dir", "down");
cleanStep["scroll_px"] = scrollPx;
cleanStep["wait_sec"] = waitSec;
cleanStep["reason"] = gs("reason", "");
cleanStep["save_key"] = gs("save_key", "");
cleanStep["save_from"] = gs("save_from", "none");
cleanStep["save_hint"] = gs("save_hint", "");

parsedSteps.Add(cleanStep);
}

if (parsedSteps.Count == 0) {
project.SendErrorToLog("Нет валидных шагов после парсинга!", true);
return "ERROR_EMPTY_STEPS";
}

// ── Вставляем WAIT(2s) после каждого CLICK (как в Python plan()) ──────
var finalSteps = new System.Collections.Generic.List<Global.ZennoLab.Json.Linq.JObject>();
int stepCounter = 1;

for (int i = 0; i < parsedSteps.Count; i++) {
var step = parsedSteps[i];
step["step_num"] = stepCounter++;
finalSteps.Add(step);

string sAct = step["action"].ToString().ToUpper();
// Добавляем WAIT только если:
// - шаг CLICK
// - это не последний шаг
// - следующий шаг уже не WAIT
if (sAct == "CLICK" && i + 1 < parsedSteps.Count) {
string nextAct = parsedSteps[i + 1]["action"] != null
? parsedSteps[i + 1]["action"].ToString().ToUpper() : "";
if (nextAct != "WAIT") {
var waitStep = new Global.ZennoLab.Json.Linq.JObject();
waitStep["step_num"] = stepCounter++;
waitStep["action"] = "WAIT";
waitStep["label"] = "auto-wait after click";
waitStep["x"] = 0;
waitStep["y"] = 0;
waitStep["value"] = "";
waitStep["scroll_dir"] = "down";
waitStep["scroll_px"] = 300;
waitStep["wait_sec"] = 2;
waitStep["reason"] = "Waiting for page/DOM to update after clicking '" + step["label"].ToString() + "'";
waitStep["save_key"] = "";
waitStep["save_from"] = "none";
waitStep["save_hint"] = "";
finalSteps.Add(waitStep);
}
}
}

// ── Строим итоговый action_chain.json (идентично Python output) ───────
var chainJson = new Global.ZennoLab.Json.Linq.JObject();

// Читаем task из task.txt для поля "task" (как в Python)
string taskForJson = "";
try {
string tf = BOT_DIR + "task.txt";
if (System.IO.File.Exists(tf))
taskForJson = System.IO.File.ReadAllText(tf, System.Text.Encoding.UTF8).Trim();
} catch { }

chainJson["task"] = taskForJson;
chainJson["task_summary"] = jObj["task_summary"] != null ? jObj["task_summary"].ToString() : "";
chainJson["confidence"] = jObj["confidence"] != null ? jObj["confidence"].ToString() : "medium";
chainJson["total_steps"] = finalSteps.Count;
chainJson["generated_at"] = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");

var finalStepsArr = new Global.ZennoLab.Json.Linq.JArray();
foreach (var s in finalSteps) finalStepsArr.Add(s);
chainJson["steps"] = finalStepsArr;

// ── Сохраняем action_chain.json ───────────────────────────────────────
System.IO.File.WriteAllText(CHAIN_FILE,
chainJson.ToString(Global.ZennoLab.Json.Formatting.Indented),
new System.Text.UTF8Encoding(false));

// ── Пишем next_action.txt (первый шаг — идентично Python) ─────────────
var first = finalSteps[0];
System.Func<string, string, string> gf = (k, d) =>
first[k] != null ? first[k].ToString() : d;

System.IO.File.WriteAllText(NEXT_FILE,
"ACTION=" + gf("action", "ERROR").ToUpper() + "\n" +
"STEP_NUM=" + gf("step_num", "1") + "\n" +
"LABEL=" + gf("label", "") + "\n" +
"X=" + gf("x", "0") + "\n" +
"Y=" + gf("y", "0") + "\n" +
"VALUE=" + gf("value", "") + "\n" +
"SCROLL_DIR=" + gf("scroll_dir", "down") + "\n" +
"SCROLL_PX=" + gf("scroll_px", "300") + "\n" +
"WAIT_SEC=" + gf("wait_sec", "2") + "\n" +
"SAVE_KEY=" + gf("save_key", "") + "\n" +
"SAVE_FROM=" + gf("save_from", "none") + "\n" +
"SAVE_HINT=" + gf("save_hint", "") + "\n",
new System.Text.UTF8Encoding(false));

// ── Лог ──────────────────────────────────────────────────────────────
string tSummary = chainJson["task_summary"] != null ? chainJson["task_summary"].ToString() : "—";
string tConf = chainJson["confidence"] != null ? chainJson["confidence"].ToString() : "?";
project.SendInfoToLog(
"action_chain.json OK | " + finalSteps.Count + " шагов | conf=" + tConf + " | " + tSummary, true);

for (int i = 0; i < finalSteps.Count; i++) {
var s = finalSteps[i];
System.Func<string, string, string> gl = (k, d) => s[k] != null ? s[k].ToString() : d;
string act = gl("action", "?").ToUpper();
string lbl = gl("label", "");
string val = gl("value", "");
string xy = (act == "CLICK" || act == "INPUT")
? " (" + gl("x","0") + "," + gl("y","0") + ")" : "";
string vv = (act == "INPUT" && val.Length > 0)
? " = '" + val.Substring(0, System.Math.Min(val.Length, 30)) + "'" : "";
project.SendInfoToLog(" " + gl("step_num","?") + ". " + act + " | " + lbl + xy + vv, false);
}

return "SUCCESS";
Инициализация первого шага из action_chain.json:
// =====================================================================
// Инициализация первого шага из action_chain.json
// =====================================================================
string BOT_DIR = project.Directory + @"\";
string CHAIN_FILE = BOT_DIR + "action_chain.json";
string NEXT_FILE = BOT_DIR + "next_action.txt";
string STATUS_FILE = BOT_DIR + "step_status.txt";

// =====================================================================
// Проверка наличия файла с планом действий
// =====================================================================
if (!System.IO.File.Exists(CHAIN_FILE)) {
project.SendErrorToLog("Файл action_chain.json не найден!", true);
return "ERROR_NO_CHAIN_FILE";
}

try {
// Очищаем статус от предыдущего выполнения задачи, чтобы избежать конфликтов
if (System.IO.File.Exists(STATUS_FILE)) {
System.IO.File.Delete(STATUS_FILE);
}

// =====================================================================
// Парсинг JSON
// =====================================================================
string jsonText = System.IO.File.ReadAllText(CHAIN_FILE, System.Text.Encoding.UTF8);
var jObj = Global.ZennoLab.Json.Linq.JObject.Parse(jsonText);
var steps = jObj["steps"] as Global.ZennoLab.Json.Linq.JArray;

if (steps == null || steps.Count == 0) {
project.SendErrorToLog("В action_chain.json пустой массив шагов (steps)!", true);
return "ERROR_EMPTY_STEPS";
}

// Ищем шаг с номером 1 (или берем самый первый, если нумерация сбилась)
Global.ZennoLab.Json.Linq.JToken firstStep = null;
foreach (var step in steps) {
if (step["step_num"] != null && step["step_num"].ToString() == "1") {
firstStep = step;
break;
}
}

if (firstStep == null) {
firstStep = steps[0];
}

// =====================================================================
// Безопасное извлечение значений с fallback-ами по умолчанию
// =====================================================================
System.Func<string, string, string> getStr = (key, def) => {
return firstStep[key] != null ? firstStep[key].ToString() : def;
};

var lines = new System.Collections.Generic.List<string>();

lines.Add("ACTION=" + getStr("action", "ERROR"));
lines.Add("STEP_NUM=" + getStr("step_num", "1"));
lines.Add("LABEL=" + getStr("label", ""));
lines.Add("X=" + getStr("x", "0"));
lines.Add("Y=" + getStr("y", "0"));
lines.Add("VALUE=" + getStr("value", ""));
lines.Add("SCROLL_DIR=" + getStr("scroll_dir", "down"));
lines.Add("SCROLL_PX=" + getStr("scroll_px", "300"));
lines.Add("WAIT_SEC=" + getStr("wait_sec", "0"));
lines.Add("SAVE_KEY=" + getStr("save_key", ""));
lines.Add("SAVE_FROM=" + getStr("save_from", "none"));
lines.Add("SAVE_HINT=" + getStr("save_hint", ""));

// =====================================================================
// Запись стартового next_action.txt и сброс флагов проекта
// =====================================================================
System.IO.File.WriteAllLines(NEXT_FILE, lines.ToArray(), System.Text.Encoding.UTF8);

// Обязательно сбрасываем переменную завершения цикла перед стартом
project.Variables["ChainDone"].Value = "false";

// Информируем в лог о задаче
string taskSummary = jObj["task_summary"] != null ? jObj["task_summary"].ToString() : "Неизвестная задача";
project.SendInfoToLog("Успех: Старт цепочки. Задача: " + taskSummary, true);

return "SUCCESS";

} catch (System.Exception ex) {
project.SendErrorToLog("Критическая ошибка при чтении/парсинге action_chain.json: " + ex.Message, true);
return "JSON_PARSE_ERROR";
}

Snippet 2 (Выполнение действия) — исполнитель с DOM-diff
Snippet 2:
// =====================================================================
// SNIPPET 2 — Action Executor + Chain Advance v5.2 (SendText fix)
// Изменения:
// - INPUT: исправлен ввод Unicode через instance.SendText() посимвольно
// (кириллица, любой язык — работает корректно)
// - Fallback: JS Native setter для React/Vue/contenteditable
// - Всё остальное без изменений от v5.2
// =====================================================================

string BOT_DIR = project.Directory + @"\";
string CHAIN_FILE = BOT_DIR + "action_chain.json";
string NEXT_FILE = BOT_DIR + "next_action.txt";
string STATUS_FILE = BOT_DIR + "step_status.txt";
string SCREEN_FILE = BOT_DIR + "screenshot.png";
string SCREEN_BEFORE = BOT_DIR + "screenshot_before.png";
string DOM_SRC_FILE = BOT_DIR + "lcm_ai_module.txt";
string DOM_BEFORE_FILE = BOT_DIR + "lcm_ai_module_before.txt";
string DOM_AFTER_FILE = BOT_DIR + "lcm_ai_module_after.txt";

// =====================================================================
// Смещения клика из переменных проекта
// =====================================================================
int OFFSET_X = 0, OFFSET_Y = 0;
try { int.TryParse(project.Variables["ClickOffsetX"].Value, out OFFSET_X); } catch { }
try { int.TryParse(project.Variables["ClickOffsetY"].Value, out OFFSET_Y); } catch { }

// =====================================================================
// Проверяем наличие chain-файла
// =====================================================================
if (!System.IO.File.Exists(CHAIN_FILE)) {
project.SendErrorToLog("action_chain.json не найден!", true);
return "ERROR_NO_CHAIN";
}

// =====================================================================
// Читаем текущий номер шага
// =====================================================================
int currentStepNum = 1;
try { int.TryParse(project.Variables["CurrentStepNum"].Value, out currentStepNum); } catch { }
if (currentStepNum < 1) currentStepNum = 1;

// =====================================================================
// Парсим action_chain.json
// =====================================================================
string chainJson = System.IO.File.ReadAllText(CHAIN_FILE, System.Text.Encoding.UTF8);
var jObj = Global.ZennoLab.Json.Linq.JObject.Parse(chainJson);
var steps = jObj["steps"] as Global.ZennoLab.Json.Linq.JArray;

if (steps == null || steps.Count == 0) {
project.SendErrorToLog("action_chain.json: пустой массив шагов!", true);
return "ERROR_EMPTY_STEPS";
}

// =====================================================================
// Ищем нужный шаг
// =====================================================================
Global.ZennoLab.Json.Linq.JToken curStep = null;
foreach (var s in steps) {
if (s["step_num"] != null && s["step_num"].ToString() == currentStepNum.ToString()) {
curStep = s; break;
}
}

if (curStep == null) {
project.SendInfoToLog("Шаг #" + currentStepNum + " не найден — все шаги выполнены.", true);
project.Variables["ChainDone"].Value = "true";
project.Variables["NeedsVerify"].Value = "false";
System.IO.File.WriteAllText(NEXT_FILE, "ACTION=DONE\nSTEP_NUM=0\nLABEL=\nX=0\nY=0\nVALUE=\n",
new System.Text.UTF8Encoding(false));
return "SUCCESS_DONE";
}

// =====================================================================
// Извлекаем параметры шага
// =====================================================================
System.Func<string, string, string> getStr = (key, def) =>
curStep[key] != null ? curStep[key].ToString() : def;

string execAction = getStr("action", "ERROR").ToUpper();
string execLabel = getStr("label", "");
string execVal = getStr("value", "");
string execSaveFrom = getStr("save_from", "none");
string execSaveKey = getStr("save_key", "");
int execX = 0; int.TryParse(getStr("x", "0"), out execX);
int execY = 0; int.TryParse(getStr("y", "0"), out execY);
int execWait = 0; int.TryParse(getStr("wait_sec", "0"), out execWait);
int execScrollPx = 0; int.TryParse(getStr("scroll_px", "300"), out execScrollPx);
string execScrollDir = getStr("scroll_dir", "down");
int totalSteps = steps.Count;

project.SendInfoToLog(">> Step #" + currentStepNum + "/" + totalSteps +
" [" + execAction + "] label='" + execLabel + "' (" + execX + "," + execY + ")", true);

// =====================================================================
// *** ДО ДЕЙСТВИЯ ***
// 1. Снимаем screenshot_before.png
// 2. Копируем DOM в lcm_ai_module_before.txt
// =====================================================================
try {
var htmlBefore = instance.ActiveTab.FindElementByTag("html", 0);
if (!htmlBefore.IsVoid) {
string b64before = htmlBefore.DrawToBitmap(false);
if (!string.IsNullOrEmpty(b64before))
System.IO.File.WriteAllBytes(SCREEN_BEFORE,
System.Convert.FromBase64String(b64before));
}
} catch (System.Exception ex) {
project.SendInfoToLog("Before screenshot err: " + ex.Message, false);
}

try {
if (System.IO.File.Exists(DOM_SRC_FILE))
System.IO.File.Copy(DOM_SRC_FILE, DOM_BEFORE_FILE, true);
} catch (System.Exception ex) {
project.SendInfoToLog("DOM before copy err: " + ex.Message, false);
}

// =====================================================================
// DOM-поиск координат для CLICK / INPUT (живой DOM)
// =====================================================================
if ((execAction == "CLICK" || execAction == "INPUT") && !string.IsNullOrEmpty(execLabel)) {

try {
// Пишем результат в textarea.value — GetValue() работает в ZP
// Вставляем логику поиска напрямую, без вложенного IIFE
string jsFindCoordInline =
"(function(){" +
"var old=document.getElementById('zp_coord');if(old)old.remove();" +
"var ta=document.createElement('textarea');ta.id='zp_coord';ta.style.display='none';" +
"document.body.appendChild(ta);" +
"try{" +
"var label=" + Global.ZennoLab.Json.Linq.JToken.FromObject(execLabel).ToString() + ";" +
"var lLow=label.toLowerCase().replace(/\\s+/g,' ').trim();" +
"var best=null,bestScore=-1;" +
"function score(el){" +
"var t=(el.innerText||el.textContent||'').replace(/\\s+/g,' ').trim().toLowerCase();" +
"var a=(el.getAttribute('aria-label')||'').toLowerCase();" +
"var p=(el.getAttribute('placeholder')||'').toLowerCase();" +
"var ti=(el.getAttribute('title')||'').toLowerCase();" +
"var id=(el.id||'').toLowerCase();" +
"var s=0;" +
"if(a===lLow||t===lLow||p===lLow||ti===lLow)s=100;" +
"else if(a.indexOf(lLow)>=0||t.indexOf(lLow)>=0||p.indexOf(lLow)>=0)s=60;" +
"else if(lLow.indexOf(a)>=0&&a.length>2)s=40;" +
"if(id.indexOf(lLow.replace(/\\s/g,''))>=0)s+=10;" +
"return s;}" +
"var tags=['button','a','input','textarea','select','[role=button]','[role=link]','[contenteditable=true]'];" +
"tags.forEach(function(sel){try{document.querySelectorAll(sel).forEach(function(el){var sc=score(el);if(sc>bestScore){bestScore=sc;best=el;}});}catch(e){}});" +
"if(best&&bestScore>0){" +
"var r=best.getBoundingClientRect();" +
"var cx=Math.round(r.left+window.scrollX+r.width/2);" +
"var cy=Math.round(r.top+window.scrollY+r.height/2);" +
"ta.value='FOUND:'+cx+':'+cy+':'+bestScore;" +
"}else{ta.value='NOTFOUND';}" +
"}catch(e){ta.value='ERR:'+e.message;}" +
"})();";
instance.ActiveTab.MainDocument.EvaluateScript(jsFindCoordInline);
System.Threading.Thread.Sleep(100);
var coordEl = instance.ActiveTab.FindElementById("zp_coord");
int coordWait = 0;
while (coordEl.IsVoid && coordWait < 20) {
System.Threading.Thread.Sleep(10);
coordEl = instance.ActiveTab.FindElementById("zp_coord");
coordWait++;
}
string domResult = coordEl.IsVoid ? null : coordEl.GetValue();
instance.ActiveTab.MainDocument.EvaluateScript(
"var e=document.getElementById('zp_coord');if(e)e.remove();"
);
project.SendInfoToLog("DOM coord [" + execLabel + "]: " + domResult, true);
if (domResult != null && domResult.StartsWith("FOUND:")) {
var parts = domResult.Split(':');
if (parts.Length >= 3) {
int nx = 0, ny = 0;
if (int.TryParse(parts[1], out nx) && int.TryParse(parts[2], out ny) && nx > 0 && ny > 0) {
project.SendInfoToLog("Coord: (" + execX + "," + execY + ") -> (" + nx + "," + ny + ")", true);
execX = nx; execY = ny;
}
}
}
} catch (System.Exception ex) {
project.SendInfoToLog("DOM coord err: " + ex.Message, false);
}
}

int cX = execX + OFFSET_X;
int cY = execY + OFFSET_Y;

// =====================================================================
// Выполнение действия
// =====================================================================
if (execAction == "CLICK") {
// ── FullEmulation: реальное движение мыши + клик ──────────────────
try {
instance.ActiveTab.FullEmulationMouseMove(cX, cY);
System.Threading.Thread.Sleep(120);
instance.ActiveTab.FullEmulationMouseClick("left", "click");
project.SendInfoToLog("CLICK (FullEmulation) at (" + cX + "," + cY + ")", true);
} catch (System.Exception ex) {
project.SendInfoToLog("FullEmulation CLICK failed (" + ex.Message + "), fallback JS", false);
string script =
"(function(){var el=document.elementFromPoint(" + cX + "," + cY + ");" +
"if(el){" +
"el.dispatchEvent(new MouseEvent('mouseover',{bubbles:true}));" +
"el.dispatchEvent(new MouseEvent('mousedown',{bubbles:true,cancelable:true}));" +
"el.dispatchEvent(new MouseEvent('mouseup',{bubbles:true,cancelable:true}));" +
"el.click();" +
"}else{console.warn('No element at " + cX + "," + cY + "');}})()";
instance.ActiveTab.MainDocument.EvaluateScript(script);
project.SendInfoToLog("CLICK (JS fallback) at (" + cX + "," + cY + ")", true);
}

} else if (execAction == "INPUT") {

// ==================================================================
// INPUT — посимвольный ввод через instance.SendText()
// Работает с любым Unicode: кириллица, латиница, спецсимволы
// Стратегия 1: FullEmulation фокус → очистка поля → SendText посимвольно
// Стратегия 2: JS Native setter (fallback для React/Vue/contenteditable)
// ==================================================================

// ── Фокус: FullEmulation клик по полю ─────────────────────────────
try {
instance.ActiveTab.FullEmulationMouseMove(cX, cY);
System.Threading.Thread.Sleep(80);
instance.ActiveTab.FullEmulationMouseClick("left", "click");
System.Threading.Thread.Sleep(300);
project.SendInfoToLog("INPUT focus at (" + cX + "," + cY + ")", true);
} catch (System.Exception ex) {
project.SendInfoToLog("FullEmulation INPUT focus failed: " + ex.Message + ", fallback JS", false);
instance.ActiveTab.MainDocument.EvaluateScript(
"(function(){var el=document.elementFromPoint(" + cX + "," + cY + ");" +
"if(el){el.focus();el.click();}})()");
System.Threading.Thread.Sleep(400);
}

// ── Очищаем поле перед вводом через JS ────────────────────────────
try {
instance.ActiveTab.MainDocument.EvaluateScript(
"(function(){" +
"var el=document.activeElement;" +
"if(!el||el===document.body)el=document.elementFromPoint(" + cX + "," + cY + ");" +
"if(!el)return;" +
"if(el.tagName==='INPUT'||el.tagName==='TEXTAREA'){" +
"var nv=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value')" +
"||Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype,'value');" +
"if(nv&&nv.set)nv.set.call(el,'');else el.value='';" +
"el.dispatchEvent(new Event('input',{bubbles:true}));}" +
"else if(el.isContentEditable){el.innerText='';}" +
"})()");
System.Threading.Thread.Sleep(80);
} catch { }

bool inputOk = false;

// ── Стратегия 1: instance.SendText() посимвольно ──────────────────
// Unicode-safe: работает с кириллицей, иероглифами, любым текстом
try {
instance.WaitFieldEmulationDelay();

var rnd = new System.Random();
char[] symbols = execVal.ToCharArray();
for (int i = 0; i < symbols.Length; i++) {
string ch = symbols[i].ToString();
int latency = rnd.Next(30, 120);
instance.SendText(ch, latency);
}
System.Threading.Thread.Sleep(200);

// Проверяем что текст появился в активном элементе
string checkJs =
"(function(){" +
"var el=document.activeElement;" +
"if(!el||el===document.body)el=document.elementFromPoint(" + cX + "," + cY + ");" +
"if(!el)return 'NO_EL';" +
"var v=el.value!==undefined?el.value:(el.innerText||el.textContent||'');" +
"v=v.replace(/\\s+/g,' ').trim();" +
"var exp=" + Global.ZennoLab.Json.Linq.JToken.FromObject(execVal.Trim()).ToString() + ";" +
"return v.indexOf(exp)>=0?'OK':'GOT:'+v.substring(0,80);" +
"})()";
string checkRes = instance.ActiveTab.MainDocument.EvaluateScript(checkJs);
project.SendInfoToLog("INPUT SendText check: " + checkRes, true);

if (checkRes == "OK") {
inputOk = true;
project.SendInfoToLog("INPUT (SendText) SUCCESS: '" + execVal + "'", true);
}
} catch (System.Exception ex) {
project.SendInfoToLog("INPUT SendText failed: " + ex.Message + " → JS Native setter", false);
}

// ── Стратегия 2: JS Native setter + Quill/Gemini специфичный поиск ─────
if (!inputOk) {
try {
string nativeJs =
"(function(){" +
"var el=null;" +
// Приоритет 1: Quill editor (Gemini использует ql-editor)
"var qls=document.querySelectorAll('.ql-editor,[class*=\"ql-editor\"]');" +
"for(var qi=0;qi<qls.length;qi++){if(qls[qi].contentEditable==='true'){el=qls[qi];break;}}" +
// Приоритет 2: поиск по ariaLabel через все элементы включая Shadow DOM
"if(!el){" +
"function findInShadow(root,depth){" +
"if(!root||depth>8)return null;" +
"var all=root.querySelectorAll('*');" +
"for(var i=0;i<all.length;i++){" +
"var e=all[i];" +
"var a=(e.getAttribute('aria-label')||'').toLowerCase();" +
"var lLow=" + Global.ZennoLab.Json.Linq.JToken.FromObject(execLabel.ToLower()).ToString() + ";" +
"if(e.contentEditable==='true'&&e.tagName!=='BODY'&&(a.indexOf(lLow)>=0||lLow.indexOf(a)>=0&&a.length>3)){return e;}" +
"if(e.shadowRoot){var r=findInShadow(e.shadowRoot,depth+1);if(r)return r;}" +
"}" +
"return null;}" +
"el=findInShadow(document,0);}" +
// Приоритет 3: любой contenteditable не body
"if(!el){var ces=document.querySelectorAll('[contenteditable=true]');for(var ci=0;ci<ces.length;ci++){if(ces[ci].tagName!=='BODY'){el=ces[ci];break;}}}" +
// Приоритет 4: по label/placeholder
"if(!el){" +
"var lLow=" + Global.ZennoLab.Json.Linq.JToken.FromObject(execLabel.ToLower()).ToString() + ";" +
"document.querySelectorAll('input,textarea,[contenteditable]').forEach(function(e){" +
"if(el)return;" +
"var p=(e.getAttribute('placeholder')||'').toLowerCase();" +
"var a=(e.getAttribute('aria-label')||'').toLowerCase();" +
"if(p.indexOf(lLow)>=0||a.indexOf(lLow)>=0)el=e;});}" +
"if(!el)return 'NOTFOUND';" +
// Если не нашли — ищем по label/placeholder в обычном DOM
"if(!el){" +
"var lLow=" + Global.ZennoLab.Json.Linq.JToken.FromObject(execLabel.ToLower()).ToString() + ";" +
"document.querySelectorAll('input,textarea,[contenteditable]').forEach(function(e){" +
"if(el)return;" +
"var p=(e.getAttribute('placeholder')||'').toLowerCase();" +
"var a=(e.getAttribute('aria-label')||'').toLowerCase();" +
"if(p.indexOf(lLow)>=0||a.indexOf(lLow)>=0)el=e;});}" +
// Последний fallback — elementFromPoint
"if(!el)el=document.elementFromPoint(" + cX + "," + cY + ");" +
"if(!el)return 'NOTFOUND';" +
"el.focus();" +
"var val=" + Global.ZennoLab.Json.Linq.JToken.FromObject(execVal).ToString() + ";" +
"el.focus();" +
"if(el.isContentEditable){" +
// Очищаем через select all + delete
"el.innerHTML='';" +
"var r=document.createRange();r.selectNodeContents(el);r.collapse(true);" +
"var s=window.getSelection();s.removeAllRanges();s.addRange(r);" +
// Вставляем через execCommand — единственный надёжный способ для Quill
"document.execCommand('selectAll',false,null);" +
"document.execCommand('delete',false,null);" +
"document.execCommand('insertText',false,val);" +
// Дополнительно диспатчим события для Quill
"el.dispatchEvent(new InputEvent('input',{bubbles:true,data:val,inputType:'insertText'}));" +
"el.dispatchEvent(new Event('change',{bubbles:true}));" +
// Проверяем что текст попал
"var got=(el.innerText||el.textContent||'').trim();" +
"return got.indexOf(val)>=0?'OK_CE':'CE_GOT:'+got.substring(0,50);}" +
// Обычные input/textarea
"var nv=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value')" +
"||Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype,'value');" +
"if(nv&&nv.set){nv.set.call(el,val);}else{el.value=val;}" +
"el.dispatchEvent(new InputEvent('input',{bubbles:true,data:val,inputType:'insertText'}));" +
"el.dispatchEvent(new Event('change',{bubbles:true}));" +
"el.dispatchEvent(new KeyboardEvent('keyup',{bubbles:true}));" +
"return 'OK_NATIVE';})()";

string nativeRes = instance.ActiveTab.MainDocument.EvaluateScript(nativeJs);
project.SendInfoToLog("INPUT NativeSetter: " + nativeRes, true);

if (nativeRes != null && nativeRes.StartsWith("OK")) {
inputOk = true;
System.Threading.Thread.Sleep(150);
project.SendInfoToLog("INPUT (NativeSetter) SUCCESS: '" + execVal + "'", true);
} else {
project.SendErrorToLog("INPUT все стратегии провалились. Result: " + nativeRes, true);
}
} catch (System.Exception ex) {
project.SendErrorToLog("INPUT NativeSetter failed: " + ex.Message, true);
}
}

project.SendInfoToLog("INPUT final: ok=" + inputOk + " | '" + execVal + "' at (" + cX + "," + cY + ")", true);

} else if (execAction == "SCROLL") {
string dir = execScrollDir == "up" ? (-execScrollPx).ToString() : execScrollPx.ToString();
instance.ActiveTab.MainDocument.EvaluateScript("window.scrollBy(0," + dir + ");");
project.SendInfoToLog("SCROLL " + execScrollDir + " " + execScrollPx + "px", true);

} else if (execAction == "NAVIGATE") {
instance.ActiveTab.Navigate(execVal, "");
if (instance.ActiveTab.IsBusy) instance.ActiveTab.WaitDownloading();
project.SendInfoToLog("NAVIGATE -> " + execVal, true);

} else if (execAction == "WAIT") {
project.SendInfoToLog("WAIT " + execWait + "s", true);
System.Threading.Thread.Sleep(execWait * 1000);

} else {
project.SendErrorToLog("Неизвестное действие: " + execAction, false);
}

// =====================================================================
// *** ПОСЛЕ ДЕЙСТВИЯ ***
// Пауза -> скриншот AFTER -> DOM AFTER
// =====================================================================
int pauseMs = execAction == "CLICK" ? 1500 : (execAction == "NAVIGATE" ? 2500 : 600);
System.Threading.Thread.Sleep(pauseMs);

// Скриншот AFTER
try {
var htmlEl = instance.ActiveTab.FindElementByTag("html", 0);
if (!htmlEl.IsVoid) {
string b64 = htmlEl.DrawToBitmap(false);
if (!string.IsNullOrEmpty(b64))
System.IO.File.WriteAllBytes(SCREEN_FILE, System.Convert.FromBase64String(b64));
}
} catch (System.Exception ex) {
project.SendInfoToLog("After screenshot err: " + ex.Message, false);
}

// DOM AFTER — копируем lcm_ai_module_before как раньше, затем быстрая проверка координат цепочки
try {
// Шаг 1: lcm_ai_module_after = текущий снапшот DOM (быстрый mapper)
string domMapperJsDirect =
"(function(){" +
"var zs_old=document.getElementById('zp_mstat');if(zs_old)zs_old.remove();" +
"var zs=document.createElement('textarea');zs.id='zp_mstat';zs.style.display='none';zs.value='PENDING';" +
"document.body.appendChild(zs);" +
"try{" +
"if(!document.body){zs.value='ERR:no body';return;}" +
"var IGNORE=new Set(['SCRIPT','STYLE','NOSCRIPT','META','HEAD','LINK','PATH','DEFS','SVG','BR','WBR','TEMPLATE']);" +
"var ITAGS=new Set(['A','BUTTON','INPUT','SELECT','TEXTAREA','DETAILS','SUMMARY','LABEL','OPTION','OPTGROUP']);" +
"var IROLES=new Set(['button','link','menuitem','tab','checkbox','radio','switch','combobox','listbox','option','treeitem','slider','spinbutton','searchbox','textbox','gridcell']);" +
"var elements=[],idCounter=0;" +
"function safeStr(v,max){if(!v)return undefined;var s=String(v).replace(/\\s+/g,' ').trim();return s.length?s.substring(0,max||300):undefined;}" +
"function getRect(el){try{var r=el.getBoundingClientRect();return{x:Math.round(r.left+window.scrollX),y:Math.round(r.top+window.scrollY),w:Math.round(r.width),h:Math.round(r.height),cx:Math.round(r.left+window.scrollX+r.width/2),cy:Math.round(r.top+window.scrollY+r.height/2)};}catch(e){return{x:0,y:0,w:0,h:0,cx:0,cy:0};}}" +
"function traverse(el,depth,sd,pid){" +
"if(!el||el.nodeType!==1)return;" +
"var tag=el.tagName?el.tagName.toUpperCase():'';" +
"if(IGNORE.has(tag))return;" +
"if(el.id&&el.id.startsWith('zp_'))return;" +
"var rect=getRect(el),nodeId=++idCounter;" +
"var role=el.getAttribute('role')||'',type=el.getAttribute('type')||'';" +
"var isI=ITAGS.has(tag)||IROLES.has(role)||el.getAttribute('onclick')!=null||el.getAttribute('tabindex')!=null||el.contentEditable==='true';" +
"var obj={id:nodeId,tag:tag.toLowerCase(),x:rect.x,y:rect.y,w:rect.w,h:rect.h,cx:rect.cx,cy:rect.cy,visible:rect.w>0&&rect.h>0};" +
"if(isI)obj.interactive=true;" +
"var innerText=safeStr(el.innerText||el.textContent,400);if(innerText)obj.text=innerText;" +
"if(el.id)obj.elId=el.id;" +
"if(el.className&&typeof el.className==='string')obj.cls=safeStr(el.className,150);" +
"var al=el.getAttribute('aria-label');if(al)obj.ariaLabel=safeStr(al,200);" +
"if(el.placeholder)obj.ph=safeStr(el.placeholder,200);" +
"if(role)obj.role=role;if(type)obj.type=type;" +
"elements.push(obj);" +
"if(el.children){for(var i=0;i<el.children.length;i++)traverse(el.children[i],depth+1,sd,nodeId);}" +
"if(el.shadowRoot&&el.shadowRoot.children){for(var i=0;i<el.shadowRoot.children.length;i++)traverse(el.shadowRoot.children[i],depth+1,sd+1,nodeId);}" +
"}" +
"traverse(document.body,0,0,null);" +
"var interactive=elements.filter(function(e){return e.interactive&&e.visible;});" +
"var meta={url:window.location.href,title:document.title,scrollX:window.scrollX,scrollY:window.scrollY,viewportW:window.innerWidth,viewportH:window.innerHeight,pageW:document.documentElement.scrollWidth,pageH:document.documentElement.scrollHeight,totalNodes:elements.length,timestamp:new Date().toISOString()};" +
"var result=JSON.stringify({meta:meta,elements:elements,interactive:interactive});" +
"var old2=document.getElementById('zp_ai_dom_result');if(old2)old2.remove();" +
"var ta=document.createElement('textarea');ta.id='zp_ai_dom_result';ta.style.display='none';ta.value=result;" +
"document.body.appendChild(ta);" +
"zs.value='OK:'+elements.length;" +
"}catch(e){zs.value='JSERROR:'+e.message;}" +
"})();";

instance.ActiveTab.MainDocument.EvaluateScript(domMapperJsDirect);
System.Threading.Thread.Sleep(300);
var mstatEl = instance.ActiveTab.FindElementById("zp_mstat");
string mapperResult = mstatEl.IsVoid ? null : mstatEl.GetValue();
instance.ActiveTab.MainDocument.EvaluateScript(
"var e=document.getElementById('zp_mstat');if(e)e.remove();"
);
project.SendInfoToLog("DOM after mapper: " + mapperResult, true);

if (mapperResult != null && mapperResult.StartsWith("OK")) {
var domRawEl = instance.ActiveTab.FindElementById("zp_ai_dom_result");
string rawDomJson = domRawEl.IsVoid ? null : domRawEl.GetValue();
instance.ActiveTab.MainDocument.EvaluateScript(
"var e=document.getElementById('zp_ai_dom_result');if(e)e.remove();"
);

if (!string.IsNullOrEmpty(rawDomJson)) {
// Сохраняем как after и обновляем основной снапшот
System.IO.File.WriteAllText(DOM_AFTER_FILE, rawDomJson, new System.Text.UTF8Encoding(false));
System.IO.File.WriteAllText(DOM_SRC_FILE, rawDomJson, new System.Text.UTF8Encoding(false));
project.SendInfoToLog("DOM after written: " + rawDomJson.Length + " bytes", true);
}
} else {
project.SendErrorToLog("DOM after mapper failed: " + mapperResult, true);
}
} catch (System.Exception ex) {
project.SendErrorToLog("DOM after FAILED: " + ex.Message, true);
try {
instance.ActiveTab.MainDocument.EvaluateScript(
"['zp_c','zp_r','zp_ai_dom_result'].forEach(function(id){var e=document.getElementById(id);if(e)e.remove();});"
);
} catch {}
}

// =====================================================================
// DOM DIFF — только проверка смещения координат элементов следующих шагов цепочки
// =====================================================================
int domChangeLevel = 0;
string domChangeSummary = "DOM не изменился";

try {
// Собираем labels следующих шагов цепочки (только CLICK/INPUT)
var chainLabels = new System.Collections.Generic.List<string>();
foreach (var s in steps) {
int sNum = 0;
int.TryParse(s["step_num"] != null ? s["step_num"].ToString() : "0", out sNum);
if (sNum <= currentStepNum) continue;
string sAct = s["action"] != null ? s["action"].ToString().ToUpper() : "";
if (sAct != "CLICK" && sAct != "INPUT") continue;
string sLbl = s["label"] != null ? s["label"].ToString() : "";
if (!string.IsNullOrEmpty(sLbl)) chainLabels.Add(sLbl.ToLower().Trim());
}

Global.ZennoLab.Json.Linq.JArray domBefore = null;
Global.ZennoLab.Json.Linq.JArray domAfter = null;

if (System.IO.File.Exists(DOM_BEFORE_FILE)) {
try {
var raw = Global.ZennoLab.Json.Linq.JObject.Parse(
System.IO.File.ReadAllText(DOM_BEFORE_FILE, System.Text.Encoding.UTF8));
// Поддерживаем оба формата: {elements:[]} и {page:{}, elements:[]}
domBefore = (raw["elements"] as Global.ZennoLab.Json.Linq.JArray)
?? (raw["interactive"] as Global.ZennoLab.Json.Linq.JArray);
} catch (System.Exception ex2) {
project.SendInfoToLog("DOM diff: before read err: " + ex2.Message, false);
}
}

if (System.IO.File.Exists(DOM_AFTER_FILE)) {
try {
var raw = Global.ZennoLab.Json.Linq.JObject.Parse(
System.IO.File.ReadAllText(DOM_AFTER_FILE, System.Text.Encoding.UTF8));
domAfter = (raw["elements"] as Global.ZennoLab.Json.Linq.JArray)
?? (raw["interactive"] as Global.ZennoLab.Json.Linq.JArray);
} catch (System.Exception ex2) {
project.SendInfoToLog("DOM diff: after read err: " + ex2.Message, false);
}
}

if (domBefore == null || domAfter == null || chainLabels.Count == 0) {
domChangeLevel = 0;
domChangeSummary = "DOM diff: нет данных или нет следующих шагов с координатами";
project.SendInfoToLog(domChangeSummary, false);
} else {
// Для каждого label из цепочки ищем элемент в before и after, сравниваем cx/cy
int movedCount = 0;
var moveDetails = new System.Text.StringBuilder();

foreach (string lbl in chainLabels) {
// Ищем лучший матч в before
Global.ZennoLab.Json.Linq.JToken bestBefore = null;
int bestScoreBefore = -1;
foreach (var el in domBefore) {
if (el["interactive"] == null) continue;
string elInter = el["interactive"].ToString().ToLower();
if (elInter != "true" && elInter != "1") continue;
if (el["visible"] == null) continue;
string elVis = el["visible"].ToString().ToLower();
if (elVis != "true" && elVis != "1") continue;
string t = (el["text"] != null ? el["text"].ToString() : "").ToLower();
string a = (el["ariaLabel"] != null ? el["ariaLabel"].ToString() : "").ToLower();
string p = (el["ph"] != null ? el["ph"].ToString() : "").ToLower();
int sc = 0;
if (a == lbl || t == lbl || p == lbl) sc = 100;
else if (a.Contains(lbl) || t.Contains(lbl) || p.Contains(lbl)) sc = 60;
else if (lbl.Contains(a) && a.Length > 2) sc = 40;
if (sc > bestScoreBefore) { bestScoreBefore = sc; bestBefore = el; }
}

// Ищем лучший матч в after
Global.ZennoLab.Json.Linq.JToken bestAfter = null;
int bestScoreAfter = -1;
foreach (var el in domAfter) {
if (el["interactive"] == null) continue;
string elInter = el["interactive"].ToString().ToLower();
if (elInter != "true" && elInter != "1") continue;
if (el["visible"] == null) continue;
string elVis = el["visible"].ToString().ToLower();
if (elVis != "true" && elVis != "1") continue;
string t = (el["text"] != null ? el["text"].ToString() : "").ToLower();
string a = (el["ariaLabel"] != null ? el["ariaLabel"].ToString() : "").ToLower();
string p = (el["ph"] != null ? el["ph"].ToString() : "").ToLower();
int sc = 0;
if (a == lbl || t == lbl || p == lbl) sc = 100;
else if (a.Contains(lbl) || t.Contains(lbl) || p.Contains(lbl)) sc = 60;
else if (lbl.Contains(a) && a.Length > 2) sc = 40;
if (sc > bestScoreAfter) { bestScoreAfter = sc; bestAfter = el; }
}

if (bestBefore == null || bestAfter == null) {
// Элемент исчез или появился — координаты невалидны
movedCount++;
moveDetails.Append("'" + lbl + "' not found in " + (bestBefore == null ? "before" : "after") + "; ");
continue;
}

int cxB = 0, cyB = 0, cxA = 0, cyA = 0;
if (bestBefore["cx"] != null) int.TryParse(bestBefore["cx"].ToString(), out cxB);
if (bestBefore["cy"] != null) int.TryParse(bestBefore["cy"].ToString(), out cyB);
if (bestAfter["cx"] != null) int.TryParse(bestAfter["cx"].ToString(), out cxA);
if (bestAfter["cy"] != null) int.TryParse(bestAfter["cy"].ToString(), out cyA);

int dx = System.Math.Abs(cxA - cxB);
int dy = System.Math.Abs(cyA - cyB);

if (dx > 15 || dy > 15) {
movedCount++;
moveDetails.Append("'" + lbl + "' moved dx=" + dx + " dy=" + dy + "; ");
}
}

if (movedCount == 0) {
domChangeLevel = 0;
domChangeSummary = "Координаты элементов цепочки не изменились (" + chainLabels.Count + " проверено)";
} else {
domChangeLevel = 2;
domChangeSummary = "Смещены элементы цепочки (" + movedCount + "/" + chainLabels.Count + "): " + moveDetails.ToString();
}

project.SendInfoToLog(
"DOM diff [" + execAction + "]: level=" + domChangeLevel +
" | " + domChangeSummary, true);
}
} catch (System.Exception exDiff) {
domChangeLevel = 1;
domChangeSummary = "DOM diff исключение: " + exDiff.Message;
project.SendInfoToLog("DOM diff EXCEPTION (level=1): " + exDiff.Message, false);
}

try { project.Variables["DomChangeLevel"].Value = domChangeLevel.ToString(); } catch { }
try { project.Variables["DomChangeSummary"].Value = domChangeSummary; } catch { }

// =====================================================================
// Сбор данных для status
// =====================================================================
string sUrl = ""; try { sUrl = instance.ActiveTab.URL; } catch { }
string sTtl = ""; try { sTtl = instance.ActiveTab.Title; } catch { }

string cap = "";
if (execSaveFrom == "url") cap = sUrl;
else if (execSaveFrom == "page_title") cap = sTtl;
else if (execSaveFrom == "input_value") cap = execVal;

if (!string.IsNullOrEmpty(execSaveKey) && !string.IsNullOrEmpty(cap)) {
try { project.Variables[execSaveKey].Value = cap; } catch { }
}

System.IO.File.WriteAllText(STATUS_FILE,
"STEP_NUM=" + currentStepNum + "\n" +
"ACTION=" + execAction + "\n" +
"STATUS=done\n" +
"CAPTURED_VALUE=" + cap + "\n" +
"CURRENT_URL=" + sUrl + "\n" +
"PAGE_TITLE=" + sTtl + "\n" +
"SCREENSHOT=" + SCREEN_FILE + "\n" +
"DOM_CHANGE_LEVEL=" + domChangeLevel + "\n",
new System.Text.UTF8Encoding(false));
project.SendInfoToLog("Status | step=" + currentStepNum + " " + execAction + " url=" + sUrl, true);

// =====================================================================
// Верификация нужна только для CLICK
// =====================================================================
bool needsVerify = (execAction == "CLICK");

// =====================================================================
// Следующий шаг
// =====================================================================
int nextStepNum = currentStepNum + 1;
Global.ZennoLab.Json.Linq.JToken nextStep = null;
foreach (var s in steps) {
if (s["step_num"] != null && s["step_num"].ToString() == nextStepNum.ToString()) {
nextStep = s; break;
}
}

if (nextStep == null) {
project.SendInfoToLog("Шаг #" + currentStepNum + " — последний.", true);
System.IO.File.WriteAllText(NEXT_FILE,
"ACTION=DONE\nSTEP_NUM=0\nLABEL=\nX=0\nY=0\nVALUE=\n" +
"SCROLL_DIR=down\nSCROLL_PX=300\nWAIT_SEC=0\n" +
"SAVE_KEY=\nSAVE_FROM=none\nSAVE_HINT=\n",
new System.Text.UTF8Encoding(false));

if (needsVerify) {
project.Variables["NeedsVerify"].Value = "true";
project.Variables["ChainNeedVerify"].Value = "true";
project.Variables["ChainDone"].Value = "false";
project.SendInfoToLog("Последний шаг CLICK — ожидаем верификации.", true);
} else {
project.Variables["NeedsVerify"].Value = "false";
project.Variables["ChainNeedVerify"].Value = "false";
project.Variables["ChainDone"].Value = "true";
project.Variables["ChainResult"].Value = "DONE";
project.SendInfoToLog("Цепочка завершена (" + execAction + ").", true);
}
return "SUCCESS_LAST_STEP";
}

// Записываем следующий шаг
System.Func<string, string, string> getNextStr = (key, def) =>
nextStep[key] != null ? nextStep[key].ToString() : def;

var nextLines = new System.Collections.Generic.List<string> {
"ACTION=" + getNextStr("action", "ERROR").ToUpper(),
"STEP_NUM=" + getNextStr("step_num", nextStepNum.ToString()),
"LABEL=" + getNextStr("label", ""),
"X=" + getNextStr("x", "0"),
"Y=" + getNextStr("y", "0"),
"VALUE=" + getNextStr("value", ""),
"SCROLL_DIR=" + getNextStr("scroll_dir", "down"),
"SCROLL_PX=" + getNextStr("scroll_px", "300"),
"WAIT_SEC=" + getNextStr("wait_sec", "0"),
"SAVE_KEY=" + getNextStr("save_key", ""),
"SAVE_FROM=" + getNextStr("save_from", "none"),
"SAVE_HINT=" + getNextStr("save_hint", ""),
};
System.IO.File.WriteAllLines(NEXT_FILE, nextLines.ToArray(), new System.Text.UTF8Encoding(false));

// Обновляем переменные
project.Variables["CurrentStepNum"].Value = nextStepNum.ToString();
project.Variables["CurrentAction"].Value = getNextStr("action", "ERROR").ToUpper();
project.Variables["CurrentLabel"].Value = getNextStr("label", "");
project.Variables["CurrentValue"].Value = getNextStr("value", "");
project.Variables["CurrentX"].Value = getNextStr("x", "0");
project.Variables["CurrentY"].Value = getNextStr("y", "0");
project.Variables["CurrentWaitSec"].Value = getNextStr("wait_sec","0");
project.Variables["CurrentSaveFrom"].Value = getNextStr("save_from","none");
project.Variables["CurrentSaveKey"].Value = getNextStr("save_key", "");
project.Variables["NeedsVerify"].Value = needsVerify ? "true" : "false";
project.Variables["ChainDone"].Value = "false";

// =====================================================================
// CoordRefresh — только если DOM реально изменился
// =====================================================================
string nextAction = getNextStr("action", "").ToUpper();
bool nextNeedsCoords = (nextAction == "CLICK" || nextAction == "INPUT");

bool coordRefreshNeeded = false;
string coordRefreshReason = "";

if (!nextNeedsCoords) {
coordRefreshNeeded = false;
coordRefreshReason = "следующий шаг [" + nextAction + "] не требует координат";
} else if (domChangeLevel == 0) {
coordRefreshNeeded = false;
coordRefreshReason = "DOM идентичен до/после — координаты актуальны";
} else if (domChangeLevel == 1 && nextAction == "INPUT") {
coordRefreshNeeded = false;
coordRefreshReason = "DOM изменился незначительно (level=1), INPUT-поля стабильны";
} else {
coordRefreshNeeded = true;
coordRefreshReason = "DOM изменился (level=" + domChangeLevel + "): " + domChangeSummary;
}

project.Variables["CoordRefreshNeeded"].Value = coordRefreshNeeded ? "true" : "false";

project.SendInfoToLog(
"CoordRefresh: " + (coordRefreshNeeded ? "НУЖЕН" : "не нужен") +
" | " + coordRefreshReason, true);

if (coordRefreshNeeded) {
var remainingSteps = new System.Text.StringBuilder();
int idx = 0;
foreach (var s in steps) {
int sNum = 0;
int.TryParse(s["step_num"] != null ? s["step_num"].ToString() : "0", out sNum);
if (sNum < nextStepNum) continue;
idx++;
System.Func<string, string, string> gs = (k, d) => s[k] != null ? s[k].ToString() : d;
string sAct = gs("action","?").ToUpper();
string sLbl = gs("label","");
string sVal = gs("value","");
string sX = gs("x","0");
string sY = gs("y","0");
string line = idx + ". " + sAct;
if (!string.IsNullOrEmpty(sLbl)) line += " on: " + sLbl;
if (!string.IsNullOrEmpty(sVal) && sAct == "INPUT") line += " (value: '" + sVal + "')";
if ((sAct == "CLICK" || sAct == "INPUT") && (sX != "0" || sY != "0"))
line += " [current coords: " + sX + "," + sY + "]";
remainingSteps.AppendLine(line);
}

string curUrl2 = ""; try { curUrl2 = instance.ActiveTab.URL; } catch { }
string curTitle = ""; try { curTitle = instance.ActiveTab.Title; } catch { }

project.Variables["CoordRefreshPrompt"].Value =
"Page state has changed. The next action requires FRESH coordinates.\n" +
"Current page URL: " + curUrl2 + "\n" +
"Current page title: " + curTitle + "\n\n" +
"Remaining steps (starting from step " + nextStepNum + ").\n" +
"Coordinates in brackets are STALE and must be recalculated.\n" +
"Return updated action_chain JSON with correct x,y for ALL remaining steps:\n\n" +
remainingSteps.ToString();

project.SendInfoToLog(
"CoordRefresh: шаг #" + nextStepNum +
" [" + nextAction + "] '" + getNextStr("label","") + "' требует актуальных координат.", true);
} else {
project.Variables["CoordRefreshPrompt"].Value = "";
}

project.SendInfoToLog(
"Next: #" + nextStepNum + " [" + getNextStr("action","?") + "] '" + getNextStr("label","") + "'" +
" | NeedsVerify=" + (needsVerify ? "true" : "false") +
" | CoordRefresh=" + (coordRefreshNeeded ? "true" : "false") +
" | DomLevel=" + domChangeLevel, true);

return "ok";
Snippet C / D / 3 — полная цепочка верификации
Snippet C:
// =====================================================================
// SNIPPET C — Verify Prompt Builder
// Формирует промпт для верификации последнего действия → VerifyPrompt
// Запускается ТОЛЬКО если NeedsVerify=true
// =====================================================================

string BOT_DIR = project.Directory + @"\";

// ── Проверяем нужность верификации ───────────────────────────────────
string needsVerify = "";
try { needsVerify = project.Variables["NeedsVerify"].Value; } catch { }

if (needsVerify != "true") {
project.SendInfoToLog("NeedsVerify=false — пропускаем формирование промпта верификации.", true);
project.Variables["VerifyOK"].Value = "true";
return "SUCCESS_SKIP_VERIFY";
}

// ── Читаем step_status.txt ────────────────────────────────────────────
string statusFile = BOT_DIR + "step_status.txt";
if (!System.IO.File.Exists(statusFile)) {
project.SendErrorToLog("step_status.txt отсутствует!", true);
return "ERROR_STATUS_NOT_FOUND";
}

var statusKv = new System.Collections.Generic.Dictionary<string, string>(
System.StringComparer.OrdinalIgnoreCase);
foreach (var line in System.IO.File.ReadAllLines(statusFile, System.Text.Encoding.UTF8)) {
int eq = line.IndexOf('=');
if (eq > 0) statusKv[line.Substring(0, eq).Trim()] = line.Substring(eq + 1).Trim();
}

System.Func<System.Collections.Generic.Dictionary<string,string>, string, string, string> gkv =
(d, k, def) => d.ContainsKey(k) ? d[k] : def;

string stepNum = gkv(statusKv, "STEP_NUM", "?");
string action = gkv(statusKv, "ACTION", "?");
string currentUrl = gkv(statusKv, "CURRENT_URL", "");
string pageTitle = gkv(statusKv, "PAGE_TITLE", "");
string domLevel = gkv(statusKv, "DOM_CHANGE_LEVEL","0");

// ── Читаем параметры текущего шага из action_chain.json ───────────────
string chainFile = BOT_DIR + "action_chain.json";
string stepLabel = ""; string stepValue = ""; string stepReason = "";

if (System.IO.File.Exists(chainFile)) {
try {
var jObj = Global.ZennoLab.Json.Linq.JObject.Parse(
System.IO.File.ReadAllText(chainFile, System.Text.Encoding.UTF8));
var steps = jObj["steps"] as Global.ZennoLab.Json.Linq.JArray;
if (steps != null) {
foreach (var s in steps) {
if (s["step_num"] != null && s["step_num"].ToString() == stepNum) {
stepLabel = s["label"] != null ? s["label"].ToString() : "";
stepValue = s["value"] != null ? s["value"].ToString() : "";
stepReason = s["reason"] != null ? s["reason"].ToString() : "";
break;
}
}
}
} catch { }
}

// ── Читаем DOM DIFF (after-файлы) ────────────────────────────────────
string domBeforeFile = BOT_DIR + "lcm_ai_module_before.txt";
string domAfterFile = BOT_DIR + "lcm_ai_module_after.txt";
string domDiffText = "(DOM diff недоступен)";

if (System.IO.File.Exists(domBeforeFile) && System.IO.File.Exists(domAfterFile)) {
try {
Global.ZennoLab.Json.Linq.JArray domBefore = null, domAfter = null;
var rb = Global.ZennoLab.Json.Linq.JObject.Parse(
System.IO.File.ReadAllText(domBeforeFile, System.Text.Encoding.UTF8));
var ra = Global.ZennoLab.Json.Linq.JObject.Parse(
System.IO.File.ReadAllText(domAfterFile, System.Text.Encoding.UTF8));
domBefore = (rb["elements"] as Global.ZennoLab.Json.Linq.JArray) ?? new Global.ZennoLab.Json.Linq.JArray();
domAfter = (ra["elements"] as Global.ZennoLab.Json.Linq.JArray) ?? new Global.ZennoLab.Json.Linq.JArray();

int cntB = domBefore.Count, cntA = domAfter.Count;

// Собираем fingerprints
var fpB = new System.Collections.Generic.HashSet<string>();
var fpA = new System.Collections.Generic.HashSet<string>();
System.Func<Global.ZennoLab.Json.Linq.JToken, string> mkFp = el => {
string tag = el["tag"] != null ? el["tag"].ToString() : "?";
string txt = el["text"] != null ? el["text"].ToString() : "";
string lbl = el["label"] != null ? el["label"].ToString() : "";
if (txt.Length > 40) txt = txt.Substring(0, 40);
if (lbl.Length > 40) lbl = lbl.Substring(0, 40);
return tag + "|" + txt + "|" + lbl;
};
foreach (var el in domBefore) { fpB.Add(mkFp(el)); }
foreach (var el in domAfter) { fpA.Add(mkFp(el)); }

var newFps = new System.Collections.Generic.List<string>();
foreach (var fp in fpA) { if (!fpB.Contains(fp)) newFps.Add(fp); }
var remFps = new System.Collections.Generic.List<string>();
foreach (var fp in fpB) { if (!fpA.Contains(fp)) remFps.Add(fp); }

var diffLines = new System.Text.StringBuilder();
diffLines.AppendLine("DOM DIFF: ДО=" + cntB + " эл., ПОСЛЕ=" + cntA + " эл.");
diffLines.AppendLine(" Появилось: " + newFps.Count + " | Исчезло: " + remFps.Count);
if (newFps.Count > 0) {
diffLines.AppendLine(" НОВЫЕ элементы:");
foreach (var fp in newFps.GetRange(0, System.Math.Min(newFps.Count, 6)))
diffLines.AppendLine(" + " + fp);
}
if (remFps.Count > 0) {
diffLines.AppendLine(" УДАЛЁННЫЕ элементы:");
foreach (var fp in remFps.GetRange(0, System.Math.Min(remFps.Count, 4)))
diffLines.AppendLine(" - " + fp);
}
if (newFps.Count == 0 && remFps.Count == 0)
diffLines.AppendLine(" [DOM БЕЗ ИЗМЕНЕНИЙ — страница идентична до и после]");

domDiffText = diffLines.ToString().Trim();
} catch (System.Exception ex) {
domDiffText = "(DOM diff ошибка: " + ex.Message + ")";
}
}

// ── Сигналы на основе объективных данных ────────────────────────────
var signals = new System.Collections.Generic.List<string>();
int domLevelInt = 0;
int.TryParse(domLevel, out domLevelInt);
if (domLevelInt == 0)
signals.Add("SIGNAL: DOM идентичен до и после действия → возможно клик не сработал");
else if (domLevelInt >= 2)
signals.Add("SIGNAL: DOM значительно изменился (level=" + domLevel + ") → действие имело эффект");
else
signals.Add("SIGNAL: DOM изменился незначительно (level=" + domLevel + ")");

// ── Собираем промпт верификации ───────────────────────────────────────
var sb = new System.Text.StringBuilder();
sb.AppendLine("You are a web automation verification judge.");
sb.AppendLine("Determine if the browser action was successfully executed.");
sb.AppendLine();
sb.AppendLine("Your ENTIRE response must be a single JSON object. No markdown, no text outside JSON.");
sb.AppendLine();
sb.AppendLine("Output EXACTLY this structure:");
sb.AppendLine("{");
sb.AppendLine(" \"verdict\": \"OK\" | \"RETRY\" | \"WAIT\" | \"ERROR\",");
sb.AppendLine(" \"confidence\": \"high\" | \"medium\" | \"low\",");
sb.AppendLine(" \"reason\": \"one sentence — main evidence for decision\",");
sb.AppendLine(" \"wait_sec\": 0,");
sb.AppendLine(" \"evidence\": \"key fact from DOM diff or page state\"");
sb.AppendLine("}");
sb.AppendLine();
sb.AppendLine("Verdict rules:");
sb.AppendLine("- OK → DOM shows new elements AND page state matches expected outcome");
sb.AppendLine("- RETRY → DOM unchanged AND action had no visible effect");
sb.AppendLine("- WAIT → Page is loading/spinner visible, set wait_sec=3-5");
sb.AppendLine("- ERROR → Something went wrong (error message appeared, wrong page, etc.)");
sb.AppendLine("- When unsure between OK and RETRY → choose RETRY (safer to retry)");
sb.AppendLine();
sb.AppendLine("=== ACTION THAT WAS PERFORMED ===");
sb.AppendLine("Step: " + stepNum);
sb.AppendLine("Action type: " + action);
sb.AppendLine("Target element: '" + stepLabel + "'");
if (!string.IsNullOrEmpty(stepValue)) sb.AppendLine("Input value: '" + stepValue + "'");
if (!string.IsNullOrEmpty(stepReason)) sb.AppendLine("Expected result: " + stepReason);
sb.AppendLine();
sb.AppendLine("=== CURRENT PAGE STATE (after action) ===");
sb.AppendLine("URL: " + currentUrl);
sb.AppendLine("Title: " + pageTitle);
sb.AppendLine("DOM change level: " + domLevel + " (0=none, 1=minor, 2=significant)");
sb.AppendLine();
sb.AppendLine("=== AUTOMATIC SIGNALS ===");
foreach (var sig in signals) sb.AppendLine(sig);
sb.AppendLine();
sb.AppendLine("=== DOM DIFF ===");
sb.AppendLine(domDiffText);

string verifyPrompt = sb.ToString().Trim();
project.Variables["VerifyPrompt"].Value = verifyPrompt;

project.SendInfoToLog("VerifyPrompt сформирован | Шаг #" + stepNum + " [" + action + "] '" + stepLabel + "'", true);
return "SUCCESS";
Snippet D:
// =====================================================================
// SNIPPET D — Verify Response Parser
// Читает VerifyResponse → парсит JSON → пишет verify_result.txt
// После этого запускается логика Snippet 3 (Step Verifier Logic)
// =====================================================================

string BOT_DIR = project.Directory + @"\";
string RESULT_FILE = BOT_DIR + "verify_result.txt";

// ── Читаем ответ ─────────────────────────────────────────────────────
string rawResp = "";
try { rawResp = project.Variables["VerifyResponse"].Value.Trim(); } catch { }

if (string.IsNullOrWhiteSpace(rawResp)) {
project.SendErrorToLog("VerifyResponse пустой! ИИ не ответил.", true);
// Пишем fallback результат — RETRY
System.IO.File.WriteAllText(RESULT_FILE,
"VERIFY_STATUS=RETRY\nVERIFY_MSG=No AI response received\nVERIFY_EVIDENCE=\nWAIT_EXTRA_SEC=0\n",
new System.Text.UTF8Encoding(false));
project.Variables["VerifyOK"].Value = "false";
project.Variables["VerifyMsg"].Value = "No AI response";
return "ERROR_EMPTY_VERIFY_RESPONSE";
}

// ── Извлекаем JSON ────────────────────────────────────────────────────
string jsonStr = rawResp;

int backtickStart = jsonStr.IndexOf("```");
if (backtickStart >= 0) {
int contentStart = jsonStr.IndexOf('\n', backtickStart);
int backtickEnd = jsonStr.LastIndexOf("```");
if (contentStart >= 0 && backtickEnd > contentStart)
jsonStr = jsonStr.Substring(contentStart + 1, backtickEnd - contentStart - 1).Trim();
}

int braceStart = jsonStr.IndexOf('{');
int braceEnd = jsonStr.LastIndexOf('}');
if (braceStart >= 0 && braceEnd > braceStart)
jsonStr = jsonStr.Substring(braceStart, braceEnd - braceStart + 1);

// ── Парсим ────────────────────────────────────────────────────────────
string verdict = "RETRY";
string confidence = "low";
string reason = "";
string evidence = "";
int waitSec = 0;

if (!string.IsNullOrWhiteSpace(jsonStr) && jsonStr.StartsWith("{")) {
try {
var jObj = Global.ZennoLab.Json.Linq.JObject.Parse(jsonStr);
verdict = jObj["verdict"] != null ? jObj["verdict"].ToString().ToUpper() : "RETRY";
confidence = jObj["confidence"] != null ? jObj["confidence"].ToString().ToLower() : "low";
reason = jObj["reason"] != null ? jObj["reason"].ToString() : "";
evidence = jObj["evidence"] != null ? jObj["evidence"].ToString() : "";
int.TryParse(jObj["wait_sec"] != null ? jObj["wait_sec"].ToString() : "0", out waitSec);
} catch (System.Exception ex) {
project.SendInfoToLog("VerifyResponse JSON parse error: " + ex.Message, false);
verdict = "RETRY";
reason = "JSON parse error: " + ex.Message;
}
} else {
// Нет JSON — пробуем найти ключевые слова
string rUp = rawResp.ToUpper();
if (rUp.Contains("\"OK\"") || rUp.Contains("VERDICT: OK")) verdict = "OK";
else if (rUp.Contains("\"WAIT\"") || rUp.Contains("VERDICT: WAIT")) verdict = "WAIT";
else if (rUp.Contains("\"ERROR\"") || rUp.Contains("VERDICT: ERROR")) verdict = "ERROR";
else verdict = "RETRY";
reason = "Parsed from text (no JSON): " + rawResp.Substring(0, System.Math.Min(rawResp.Length, 100));
}

// ── Нормализуем verdict ───────────────────────────────────────────────
if (verdict != "OK" && verdict != "WAIT" && verdict != "RETRY" && verdict != "ERROR")
verdict = "RETRY";

// ── Пишем verify_result.txt ───────────────────────────────────────────
System.IO.File.WriteAllText(RESULT_FILE,
"VERIFY_STATUS=" + verdict + "\n" +
"VERIFY_MSG=" + reason + "\n" +
"VERIFY_EVIDENCE=" + evidence + "\n" +
"WAIT_EXTRA_SEC=" + waitSec + "\n" +
"CONFIDENCE=" + confidence + "\n",
new System.Text.UTF8Encoding(false));

// ── Обновляем переменные ─────────────────────────────────────────────
project.Variables["VerifyOK"].Value = verdict == "OK" ? "true" : "false";
project.Variables["VerifyMsg"].Value = reason;

project.SendInfoToLog("Verify=" + verdict + " [" + confidence + "] | " + reason +
(evidence.Length > 0 ? " | " + evidence.Substring(0, System.Math.Min(evidence.Length, 80)) : ""), true);

return "SUCCESS";
Snippet 3:
// =====================================================================
// SNIPPET 3 — Step Verifier Logic v2 (без Python, только файлы)
// Читает verify_result.txt и выполняет логику:
// OK → продолжаем цепочку
// WAIT → ждём и формируем новый VerifyPrompt (повтор верификации)
// RETRY/ERROR → формируем новый LCM промпт (перепланирование)
//
// ВАЖНО: этот сниппет запускается ПОСЛЕ Snippet D
// =====================================================================

string BOT_DIR = project.Directory + @"\";
string RESULT_FILE = BOT_DIR + "verify_result.txt";
string NEXT_FILE = BOT_DIR + "next_action.txt";
string SCREEN4 = BOT_DIR + "screenshot.png";

// ── Если верификация не нужна ─────────────────────────────────────────
string needsVerify = "";
try { needsVerify = project.Variables["NeedsVerify"].Value; } catch { }

if (needsVerify != "true") {
project.SendInfoToLog("NeedsVerify=false — пропускаем логику верификации.", true);
if (System.IO.File.Exists(NEXT_FILE)) {
string nxt = System.IO.File.ReadAllText(NEXT_FILE, System.Text.Encoding.UTF8);
if (nxt.Contains("ACTION=DONE")) {
project.Variables["ChainDone"].Value = "true";
project.SendInfoToLog("Следующий шаг DONE — цепочка завершена.", true);
}
}
project.Variables["VerifyOK"].Value = "true";
return "SUCCESS_SKIP_VERIFY";
}

// ── Читаем verify_result.txt ──────────────────────────────────────────
if (!System.IO.File.Exists(RESULT_FILE)) {
project.SendErrorToLog("verify_result.txt не найден! Сначала запустите Snippet D.", true);
return "ERROR_MISSING_RESULT_FILE";
}

var vd = new System.Collections.Generic.Dictionary<string, string>(
System.StringComparer.OrdinalIgnoreCase);
foreach (var line in System.IO.File.ReadAllLines(RESULT_FILE, System.Text.Encoding.UTF8)) {
int eq = line.IndexOf('=');
if (eq > 0) vd[line.Substring(0, eq).Trim()] = line.Substring(eq + 1).Trim();
}
System.Func<string, string, string> gv = (k, d) => vd.ContainsKey(k) ? vd[k] : d;

string vs = gv("VERIFY_STATUS", "ERROR");
string vm = gv("VERIFY_MSG", "");
string ve = gv("VERIFY_EVIDENCE", "");
int vws = 0;
int.TryParse(gv("WAIT_EXTRA_SEC", "0"), out vws);

project.Variables["VerifyOK"].Value = vs == "OK" ? "true" : "false";
project.Variables["VerifyMsg"].Value = vm;

project.SendInfoToLog("Verify=" + vs + " | " + vm +
(ve.Length > 0 ? " | " + ve.Substring(0, System.Math.Min(ve.Length, 80)) : ""), true);

// ══════════════════════════════════════════════════════════════════════
// OK — продолжаем цепочку
// ══════════════════════════════════════════════════════════════════════
if (vs == "OK") {
project.Variables["NeedsVerify"].Value = "false";
project.Variables["ChainNeedVerify"].Value = "false";

if (System.IO.File.Exists(NEXT_FILE)) {
string nxtContent = System.IO.File.ReadAllText(NEXT_FILE, System.Text.Encoding.UTF8);
if (nxtContent.Contains("ACTION=DONE")) {
project.Variables["ChainDone"].Value = "true";
project.Variables["ChainResult"].Value = "DONE";
project.SendInfoToLog("Верификация OK + DONE — цепочка полностью завершена.", true);
} else {
project.SendInfoToLog("Верификация OK — продолжаем цепочку.", true);
}
}
return "SUCCESS_OK";
}

// ══════════════════════════════════════════════════════════════════════
// WAIT — ждём и сигнализируем повторить верификацию
// ══════════════════════════════════════════════════════════════════════
if (vs == "WAIT") {
project.SendInfoToLog("Верификатор: WAIT " + vws + "s — ждём...", true);
System.Threading.Thread.Sleep(vws > 0 ? vws * 1000 : 3000);

// Обновляем скриншот
try {
var htmlEl = instance.ActiveTab.FindElementByTag("html", 0);
if (!htmlEl.IsVoid) {
string b64 = htmlEl.DrawToBitmap(false);
if (!string.IsNullOrEmpty(b64))
System.IO.File.WriteAllBytes(SCREEN4, System.Convert.FromBase64String(b64));
}
} catch { }

// Обновляем URL/Title в step_status.txt
string wu = ""; try { wu = instance.ActiveTab.URL; } catch { }
string wt = ""; try { wt = instance.ActiveTab.Title; } catch { }
string statusPath = BOT_DIR + "step_status.txt";
try {
var wLines = System.IO.File.ReadAllLines(statusPath);
for (int wi = 0; wi < wLines.Length; wi++) {
if (wLines[wi].StartsWith("CURRENT_URL=")) wLines[wi] = "CURRENT_URL=" + wu;
if (wLines[wi].StartsWith("PAGE_TITLE=")) wLines[wi] = "PAGE_TITLE=" + wt;
if (wLines[wi].StartsWith("SCREENSHOT=")) wLines[wi] = "SCREENSHOT=" + SCREEN4;
}
System.IO.File.WriteAllLines(statusPath, wLines, System.Text.Encoding.UTF8);
} catch { }

// NeedsVerify остаётся true — сигнализируем повторить Snippet C + D
// Возвращаем специальный код чтобы ZP знал — нужен повтор верификации
project.Variables["NeedsVerify"].Value = "true";
project.SendInfoToLog("WAIT: Snippet C нужно запустить снова для повторной верификации.", true);
return "WAIT_RETRY_VERIFY";
}

// ══════════════════════════════════════════════════════════════════════
// RETRY / ERROR — перепланирование через новый LCM промпт
// ══════════════════════════════════════════════════════════════════════
if (vs == "RETRY" || vs == "ERROR") {

int failedStep = 1;
try { int.TryParse(project.Variables["CurrentStepNum"].Value, out failedStep); } catch { }
failedStep = System.Math.Max(1, failedStep - 1);

project.SendInfoToLog("Верификатор " + vs + " на шаге #" + failedStep +
" — формируем новый LCM промпт | " + vm, true);

// Читаем цепочку для формирования нового task.txt
Global.ZennoLab.Json.Linq.JObject jObjR = null;
Global.ZennoLab.Json.Linq.JArray stepsR = null;
try {
string cj = System.IO.File.ReadAllText(BOT_DIR + "action_chain.json", System.Text.Encoding.UTF8);
jObjR = Global.ZennoLab.Json.Linq.JObject.Parse(cj);
stepsR = jObjR["steps"] as Global.ZennoLab.Json.Linq.JArray;
} catch { }

var taskBuilder = new System.Text.StringBuilder();
string failedLabel = "", failedAction = "", failedValue = "";

if (stepsR != null) {
foreach (var s in stepsR) {
int sn = 0;
int.TryParse(s["step_num"] != null ? s["step_num"].ToString() : "0", out sn);
if (sn == failedStep) {
failedAction = s["action"] != null ? s["action"].ToString().ToUpper() : "?";
failedLabel = s["label"] != null ? s["label"].ToString() : "";
failedValue = s["value"] != null ? s["value"].ToString() : "";
break;
}
}
}

string curUrl = ""; try { curUrl = instance.ActiveTab.URL; } catch { }

taskBuilder.AppendLine("Previous attempt failed. Step " + failedStep +
" [" + failedAction + "] target='" + failedLabel + "'" +
(failedValue.Length > 0 ? " value='" + failedValue + "'" : "") +
" did not verify: " + vm + ".");
taskBuilder.AppendLine("Current browser URL: " + curUrl);
taskBuilder.AppendLine("Please redo the following tasks from scratch:");
taskBuilder.AppendLine("");

if (stepsR != null) {
int lineNum = 1;
foreach (var s in stepsR) {
int sn = 0;
int.TryParse(s["step_num"] != null ? s["step_num"].ToString() : "0", out sn);
if (sn < failedStep) continue;
System.Func<string, string, string> gs = (k, d) => s[k] != null ? s[k].ToString() : d;
string act = gs("action","?").ToUpper();
string lbl = gs("label","");
string val = gs("value","");
string line = lineNum + ". ";
if (act == "CLICK") line += "Click on: " + lbl;
else if (act == "INPUT") line += "Type '" + val + "' into: " + lbl;
else if (act == "NAVIGATE") line += "Navigate to: " + val;
else if (act == "WAIT") line += "Wait " + gs("wait_sec","0") + " seconds";
else if (act == "SCROLL") line += "Scroll " + gs("scroll_dir","down");
else line += act + " " + lbl;
taskBuilder.AppendLine(line);
lineNum++;
}
}

string newTask = taskBuilder.ToString().Trim();
try {
System.IO.File.WriteAllText(BOT_DIR + "task.txt", newTask,
new System.Text.UTF8Encoding(false));
project.SendInfoToLog("Новый task.txt записан для LCM retry.", false);
} catch { }

// Обновляем скриншот и DOM для нового LCM промпта
try {
var htmlElR = instance.ActiveTab.FindElementByTag("html", 0);
if (!htmlElR.IsVoid) {
string b64r = htmlElR.DrawToBitmap(false);
if (!string.IsNullOrEmpty(b64r))
System.IO.File.WriteAllBytes(BOT_DIR + "screenshot.png",
System.Convert.FromBase64String(b64r));
}
} catch { }

// ── Сигнализируем ZennoPoster: нужен новый LCM цикл ──────────────
// Сбрасываем состояние чтобы Snippet A сформировал новый AiPrompt
project.Variables["CurrentStepNum"].Value = "1";
project.Variables["NeedsVerify"].Value = "false";
project.Variables["ChainDone"].Value = "false";
project.Variables["VerifyOK"].Value = "false";
project.Variables["ChainNeedVerify"].Value = "false";
project.Variables["CurrentAction"].Value = "";
project.Variables["CurrentLabel"].Value = "";
project.Variables["CurrentValue"].Value = "";
project.Variables["AiResponse"].Value = ""; // очищаем старый ответ

// Удаляем screenshot_before чтобы новый шаг 1 получил корректный diff
try {
string sbPath = BOT_DIR + "screenshot_before.png";
if (System.IO.File.Exists(sbPath)) System.IO.File.Delete(sbPath);
} catch { }

project.SendInfoToLog("RETRY: переменные сброшены. Запустите Snippet A → отправьте AiPrompt в ИИ → запустите Snippet B.", true);
return "RETRY_LCM_NEEDED";
}

 return "SUCCESS";


Автоматизируйте с умом — и пусть ваши боты думают сами!
 

Вложения

Последнее редактирование:
  • Спасибо
Реакции: deskuznetsov

Кто просматривает тему: (Всего: 1, Пользователи: 0, Гости: 1)