Все исходники в одном месте
task_chain_planner.py — 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 (скрипт сброса состояния) — инициализация новой цепочки
// =====================================================================
// 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 для 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 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 — 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 — 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 — 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
// =====================================================================
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 — 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 — 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 — 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 — 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";
|