ZennoPoster AI Agent - Передаем управление браузером нейросети (С# + OpenAI compatible API)

MelD

Client
Регистрация
10.10.2019
Сообщения
2
Благодарностей
3
Баллы
3
141154





Идея создания этого проекта родилась из-за периодически встречающихся на форуме просьб добавить инструменты ИИ в ZennoPoster «из коробки» и периодических споров о том, как должна такая интеграция выглядеть. Пока официальная интеграция находится в разработке, архитектура программы и поддержка C# дают нам некоторую свободу действий — это позволяет реализовать собственную вариацию ИИ-агента непосредственно внутри ZennoPoster самостоятельно.

В рамках предыдущих конкурсов темы ИИ-агентов уже поднимались, однако в большинстве случаев решения были либо заточены под слишком узкие задачи, либо неудобны для внедрения в реальные проекты из-за избыточного количества «кубиков» и сложности логики.

Я поставил перед собой цель создать решение, которое было бы:

  • Простым в освоении — работа строится на базе современных агентных режимов ИИ
  • Гибким — архитектура позволяет адаптировать агента под сложные, многоуровневые задачи
  • Удобным для интеграции — проект не перегружен визуальными элементами и легко встраивается в существующие шаблоны



Если не интересны технические детали реализации, более гибкие настройки, а только шаблон и примеры использования — листайте к разделу 7.



Содержание

  1. Как ИИ-агент видит браузер и взаимодействует с ним — архитектура цикла
  2. Структура и описание функций (Functions): что агент умеет делать
  3. Провайдеры: OpenAI, DeepSeek, локальные модели
  4. Контроль расходов
  5. Создание повторяемых сценариев
  6. Устойчивость: autoRecovery и flex-режим
  7. Код для работы, плагин, примеры использования


1. Как ИИ-агент видит браузер и взаимодействует с ним

Первое, с чем необходимо разобраться — как организовать эффективный диалог между браузером и искусственным интеллектом.

Сейчас много говорят об ИИ-агентах, способных выполнять «всю работу» за пользователя. Зачастую под капотом таких систем лежит MCP (Model Context Protocol) — протокол, где отдельный сервер хранит описание доступных функций программы. Это удобный стандарт для масштабируемых систем, но для нашей задачи создание полноценного MCP-сервера было бы избыточным.

Вместо этого мы пойдём по пути Function Calling: вместе с основным промптом модели передаётся массив tools — описание функций, которые ИИ может вызвать для выполнения действий или получения данных.


141156

Схема итерационного цикла агента

В такой архитектуре процесс превращается в итерационный цикл:

  1. Отправка промпта (либо результата выполнения предыдущего действия). При желании первое же сообщение можно сопроводить контекстом, например скриншотом или же кодом HTML.
  2. Анализ и выбор действия: модель сама выбирает, что запросить — исходный код страницы, текстовое содержимое или скриншот — в зависимости от того, какой способ лучше подходит для текущего состояния страницы, анализирует ситуацию и выбирает следующую функцию (например, клик или ввод текста)
  3. Выполнение действия через C# внутри ZennoPoster
  4. Получение результата и отправка его обратно модели

Цикл повторяется шаг за шагом до тех пор, пока задача не будет выполнена; не возникнет критическая ошибка или не сработают ограничения из настроек.



2. Структура и описание функций (Functions)

Ключевым этапом реализации является составление описания доступных инструментов. Поскольку ZennoPoster обладает большим количеством методов, описания функций хранятся в формате JSON и разделены по логическим группам. Такой модульный подход даёт несколько преимуществ:

  • Гибкость: можно отключать группы инструментов для моделей без поддержки Vision или в сценариях с ограниченным контекстом
  • Экономия токенов: передавая только нужные инструменты, мы уменьшаем размер контекстного окна и повышаем точность выбора функции

ГруппаИнструментыКогда нужна
Browsernavigate, go_back, reload_page, get_current_url, get_page_title, clear_cookies, clear_cacheВзаимодействие с браузером
MouseKeyboardclick, mouse_click, keyboard, scroll_into_view, focus_element, mouse_move, mouse_drag, mouse_scrollКлики, ввод текста
DomReadingget_dom_text, get_page_html, get_element, get_element_countПарсинг данных
FormInputset_value, rise_eventЗаполнение форм
Screenshotstake_screenshotВизуальный анализ, autoRecovery (Aгент решает сам — использовать скриншот или читать DOM, можно принуждать)
ZennoStoragesave_variable, get_variable, list_add, list_remove, list_get, list_clear, table_set_cell ...Работа с данными ZennoPoster
AgentControlwait, report_errorВсегда, не отключается

report_error — способ агента сообщить, что задача невыполнима. Модель вызывает его сама, если заходит в тупик, и выполнение немедленно останавливается. Важно: autoRecovery при этом не срабатывает — это всегда финальная ошибка.​

Фактически эта таблица — это то, что ИИ-агент умеет делать. При желании список можно расширять: добавить описание в нужный раздел и реализовать соответствующий метод.

Два примера того, как инструменты описываются в коде:

C#:
private static readonly JArray ToolsBrowser = JArray.Parse(@"[
    {
        ""type"": ""function"",
        ""name"": ""navigate"",
        ""description"": ""Navigate the browser to a URL. Waits for the page to finish loading."",
        ""parameters"": {
            ""type"": ""object"",
            ""properties"": {
                ""url"": { ""type"": ""string"", ""description"": ""Full URL including scheme, e.g. https://example.com"" }
            },
            ""required"": [""url""],
            ""additionalProperties"": false
        }
    },
    {
        ""type"": ""function"",
        ""name"": ""go_back"",
        ""description"": ""Navigate back to the previous page in browser history."",
        ""parameters"": { ""type"": ""object"", ""properties"": {}, ""additionalProperties"": false }
    }
]");

C#:
private static readonly JArray ToolsDomReading = JArray.Parse(@"[
    {
        ""type"": ""function"",
        ""name"": ""get_dom_text"",
        ""description"": ""Return the visible text content of the current page (innerText)."",
        ""parameters"": {
            ""type"": ""object"",
            ""properties"": {
                ""max_length"": { ""type"": ""integer"", ""description"": ""Max characters to return. Default 5000."" }
            },
            ""additionalProperties"": false
        }
    },
    {
        ""type"": ""function"",
        ""name"": ""get_element"",
        ""description"": ""Find an element and return its text, inner HTML, or an attribute value."",
        ""parameters"": {
            ""type"": ""object"",
            ""properties"": {
                ""by"":          { ""type"": ""string"", ""enum"": [""xpath"", ""css""] },
                ""selector"":    { ""type"": ""string"" },
                ""return_type"": { ""type"": ""string"", ""enum"": [""text"", ""html"", ""attribute""] },
                ""attribute"":   { ""type"": ""string"" },
                ""index"":       { ""type"": ""integer"" }
            },
            ""required"": [""by"", ""selector"", ""return_type""],
            ""additionalProperties"": false
        }
    }
]");

После получения ответа от модели метод-маршрутизатор направляет вызов к нужному методу ZennoPoster:

C#:
public string ExecuteTool(string toolName, JObject args)
{
    switch (toolName)
    {
        case "navigate":          return ToolNavigate(args);
        case "go_back":           return ToolGoBack();
        case "reload_page":       return ToolReloadPage();
        case "get_current_url":   return ToolGetCurrentUrl();
        case "get_page_title":    return ToolGetPageTitle();
        case "click":             return ToolClick(args);
        case "mouse_click":       return ToolMouseClick(args);
        case "keyboard":          return ToolKeyboard(args);
        case "get_dom_text":      return ToolGetDomText(args);
        case "get_element":       return ToolGetElement(args);
        case "set_value":         return ToolSetValue(args);
        // ... остальные инструменты
        default:
            return "Unknown tool: " + toolName;
    }
}

C#:
private string ToolMouseClick(JObject args)
{
int modelX = args["x"]?.ToObject() ?? 0;
int modelY = args["y"]?.ToObject() ?? 0;

// если был скриншот с масштабированием
double scale = _lastScreenshot?.ScaleFactor ?? 1.0;

int realX = (int)(modelX * scale);
int realY = (int)(modelY * scale);

string button = args["button"]?.ToString() ?? "left";

_instance.ActiveTab.FullEmulationMouseMove(realX, realY);
Thread.Sleep(100);
_instance.ActiveTab.FullEmulationMouseClick(button, "click");
_instance.ActiveTab.WaitDownloading();

return $"Clicked ({realX},{realY}) {button}";
}

Размер скриншота определяется через текущий viewport (видимая область страницы в браузере) браузера (делаем таким образом, чтобы мы отправляли действительно же самое что и видит пользователь, это позволяет сократить количество токенов, а также использовать модели которые не могут обрабатывать слишком большие изображения, немного большее описание этого будет позднее. (Но при этом у нас есть в коде возможность включения опции отправки сразу полного скриншота страницы, для более быстрой ориентации ИИ агента на странице)
Размер viewport определяется через выполнение JavaScript в браузере:
C#:
string raw = _instance.ActiveTab.MainDocument
.EvaluateScript("return window.innerWidth + ' x ' + window.innerHeight;");
Полученные значения парсятся и используются как фактический размер видимой области страницы.

Если выполнение скрипта не удалось (например, страница ещё не загружена), используется запасной вариант - получение размеров экрана из профиля браузера:

C#:
return new ViewportSize(
_project.Profile.ScreenSizeWidth,
_project.Profile.ScreenSizeHeight);
Также предусмотрена работа со сжатием изображения до нужных размеров, чтобы модель помещалась в контекст более простых моделей (но лучше всего конечно использовать оригинальное разрешения, если модель это позволяет)

C#:
        public ScreenshotData TakeScreenshotData()
        {
            ViewportSize vp;
            PageOffset   offset;

            if (_sendFullPageScreenshot)
            {
                vp     = GetFullDocumentSize();
                offset = new PageOffset(0, 0);
            }
            else
            {
                vp     = GetViewportSize();
                offset = GetPageOffset();
            }

            string tempPath   = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".png");
            string resizePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + "_r.png");

            try
            {
                ZennoPoster.ImageProcessingCropFromScreenshot(
                    instancePort: _instance.Port,
                    savePath:     tempPath,
                    cropWidth:    vp.Width,
                    cropHeight:   vp.Height,
                    leftBorder:   offset.X,
                    topBorder:    offset.Y,
                    units:        "pixel",
                    quality:      _screenshotQuality
                );

                double scaleFactor = 1.0;
                int    finalWidth  = vp.Width;
                int    finalHeight = vp.Height;
                string pathToRead  = tempPath;

                if (_maxImageDimension > 0 &&
                    (vp.Width > _maxImageDimension || vp.Height > _maxImageDimension))
                {
                    if (vp.Width >= vp.Height)
                    {
                        scaleFactor = (double)vp.Width / _maxImageDimension;
                        finalWidth  = _maxImageDimension;
                        finalHeight = (int)(vp.Height / scaleFactor);
                    }
                    else
                    {
                        scaleFactor = (double)vp.Height / _maxImageDimension;
                        finalHeight = _maxImageDimension;
                        finalWidth  = (int)(vp.Width / scaleFactor);
                    }

                    ZennoPoster.ImageProcessingResizeFromFile(
                        tempPath, resizePath, finalWidth, finalHeight, "pixel", true, true);
                    pathToRead = resizePath;
                }

                return new ScreenshotData
                {
                    Base64      = Convert.ToBase64String(File.ReadAllBytes(pathToRead)),
                    ScaleFactor = scaleFactor,
                    Width       = finalWidth,
                    Height      = finalHeight
                };
            }
            finally
            {
                if (File.Exists(tempPath))   File.Delete(tempPath);
                if (File.Exists(resizePath)) File.Delete(resizePath);
            }
        }



[HR]

3. Провайдеры: OpenAI, DeepSeek, локальные модели

В коде связь с моделью реализована через интерфейс IApiProvider — это позволяет менять провайдера не затрагивая основную логику агента. На данный момент реализовано два провайдера:

ПровайдерЭндпоинтОсобенности
OpenAiCompatibleResponsesProvider/v1/responsesИспользуется по умолчанию. Отправляет только новые сообщения через previous_response_id. Поддерживает flex и prompt caching
OpenAiCompatibleCompletionsProvider/v1/chat/completionsУниверсальный. Совместим с DeepSeek, Mistral, LM Studio, Ollama. В каждом запросе передаётся полная история диалога

При работе с OpenAiCompatibleCompletionsProvider стоит проверять поддержку Function Calling у конкретной модели — у некоторых она работает нестабильно.

C#:
// DeepSeek
var options = new ZennoposterAiAgentOptions
{
    ApiKey   = project.Variables["deepseek_key"].Value,
    Model    = "deepseek-chat",
    Provider = new OpenAiCompatibleCompletionsProvider(
                   "https://api.deepseek.com/v1/chat/completions"),
};

// Локальная модель через LM Studio (ключ не нужен)
var optionsLocal = new ZennoposterAiAgentOptions
{
    ApiKey   = "",
    Model    = "qwen/qwen3.5-9b",
    Provider = new OpenAiCompatibleCompletionsProvider(
                   "http://localhost:1234/v1/chat/completions"),
};

// OpenAI — провайдер указывать не нужно, подставляется автоматически
var optionsOpenAi = new ZennoposterAiAgentOptions
{
    ApiKey = project.Variables["openai_key"].Value,
    Model  = "gpt-5.4",
};


4. Контроль расходов

Всё взаимодействие с моделью измеряется в токенах — от этого напрямую зависит стоимость выполнения задачи. В агенте реализовано несколько механизмов контроля.

Лимиты выполнения

  • MaxSteps — максимальное число вызовов API за один запуск. По умолчанию 5. Значение -1 — без ограничений
  • MaxTokensLimit — жёсткий лимит суммарных токенов; при превышении запуск прерывается. Значение -1 — без лимита (по умолчанию). Учитывается общий объём контекста без разбивки на кешированные/некешированные токены — это сознательное упрощение

Подсчёт стоимости

Если задать цены на токены — агент будет выводить примерную стоимость каждого запуска в лог и возвращать её в результате:

  • InputPricePerMillion — цена входных токенов в USD за 1 млн
  • CachedInputPricePerMillion — цена кешированных входных токенов
  • OutputPricePerMillion — цена выходных токенов

Результаты после выполнения:

  • result.TotalTokens — суммарное число токенов
  • result.TotalInputTokens, result.TotalCachedInputTokens, result.TotalOutputTokens — разбивка по типам
  • result.CostFormatted — итоговая стоимость в виде строки, например $0.004231

Конкретные цифры привести сложно — всё зависит от сложности задачи и контекста страницы. Ориентир: при ToolGroups.All описание инструментов занимает около 2800 токенов в первом запросе. На последующих шагах эта часть кешируется и тарифицируется значительно дешевле.

Ограничение набора инструментов

Если задача не требует всех групп — можно передавать только нужные:

C#:
// Только навигация и чтение DOM
ToolGroups = ToolGroups.Browser | ToolGroups.DomReading

// Все группы кроме хранилища ZennoPoster
ToolGroups = ToolGroups.NoStorage
C#:
All           = Browser | MouseKeyboard | DomReading | FormInput | Screenshots | ZennoStorage
NoStorage     = Browser | MouseKeyboard | DomReading | FormInput | Screenshots
NoScreenshots = Browser | MouseKeyboard | DomReading | FormInput | ZennoStorage
Minimal       = Browser | MouseKeyboard | DomReading | FormInput

Важно: инструменты wait и report_error добавляются всегда и не зависят от выбранных ToolGroups.​

Кеширование промпта

Поскольку описание инструментов не меняется от запуска к запуску, OpenAI может кешировать эту часть контекста и тарифицировать её по сниженной ставке. Два параметра управляют этим поведением:

  • PromptCacheKey — строковый ключ, идентифицирующий неизменяемую часть контекста. Одинаковый ключ на всех запусках гарантирует переиспользование кеша
  • ExtendedCacheRetention — увеличивает время хранения кеша с нескольких минут до 24 часов. Актуально при редких или ночных запусках

C#:
var options = new ZennoposterAiAgentOptions
{
    PromptCacheKey         = "my-project-tools-v1",
    ExtendedCacheRetention = true,
    // ...
};
Оба параметра поддерживаются только провайдером OpenAiCompatibleResponsesProvider. При использовании OpenAiCompatibleCompletionsProvider они игнорируются. Разница в цене между обычными и кешированными токенами обычно составляет 4–10 раз в зависимости от модели.


Начальный контекст страницы (InitialContext)

Агент может сразу получить текущее состояние страницы без первого шага через параметр InitialContext:

  • None — контекст не передаётся (по умолчанию)
  • Screenshot — прикрепляется скриншот
  • PageHtml — передаётся очищенный HTML страницы
  • DomText — передаётся только текст (innerText)

Это позволяет сократить количество шагов и ускорить выполнение задачи, особенно при работе с простыми страницами. Также часто более простые модели, хуже оптимизированные для работы в качестве ИИ агентов, вроде chatgpt-nano, работают лучше при получении контекста в самом начале, в противном случае могут выдавать ошибку.


Управление объёмом HTML
При работе с реальными сайтами исходный код страницы может занимать десятки тысяч символов — большую часть из которых составляют <script>, <style>, SVG-иконки и инлайновые обработчики событий, бесполезные для навигации. Параметр HtmlCleanOptions управляет тем, что именно отправляется модели при вызове get_page_html, get_element (с return_type=html), а также при использовании начального контекста.
C#:
// Настройки по умолчанию (рекомендуется для большинства задач)
options.HtmlCleanOptions = new HtmlCleanOptions
{
    RemoveScripts         = true,   // удалять <script>
    RemoveStyles          = true,   // удалять <style>
    RemoveComments        = true,   // удалять <!-- комментарии -->
    RemoveNoscript        = true,   // удалять <noscript>
    RemoveLinkTags        = true,   // удалять <link rel="stylesheet"> и preload
    RemoveSvg             = false,  // SVG может нести семантику (иконки с aria-label)
    RemoveMeta            = false,  // meta нужна для og:title, description и т.п.
    RemoveStyleAttributes = false,  // атрибуты обычно не мешают
    RemoveEventAttributes = false,  // onclick="" помогает понять интерактивность
    CollapseWhitespace    = true,   // схлопывать лишние пробелы и пустые строки
    DefaultMaxLength      = 8000,   // лимит символов после очистки
};
либо можно использовать три готовые предустановки для быстрого использования:

ПредустановкаDefaultMaxLengthЧто удаляетсяКогда использовать
new HtmlCleanOptions()8000script, style, комментарии, noscript, linkПо умолчанию, большинство задач
HtmlCleanOptions.Minimal8000script, style, комментарии, noscriptСтраницы с важными SVG или meta
HtmlCleanOptions.Aggressive5000Всё лишнее + атрибуты style/onclick + svg + metaПростые формы, листинги
HtmlCleanOptions.Noneбез лимитаНичегоОтладка, нужен полный HTML

C#:
// Лимит символов
options.SetHtmlMaxLength(project.Variables["html_max_length"].Value); // "5000", "-1"

// Предустановка целиком
options.SetHtmlCleanPreset(project.Variables["html_preset"].Value); // "Default", "Minimal", "Aggressive", "None"

// Комбо: предустановка + точная подстройка лимита
options.SetHtmlCleanPreset("minimal")
       .SetHtmlMaxLength("12000");
Параметр max_length, передаваемый моделью, имеет приоритет над настройкой DefaultMaxLength в HtmlCleanOptions. Это позволяет модели динамически запрашивать больше или меньше контекста при необходимости.

Оптимизация работы со скриншотами

Предусмотрена работа с оптимизацией скриншотов, можно сделать так, чтобы агент не отправлял скриншоты в исходном разрешении, в случаях когда, модель для обработки имеет ограниченный контекст.

Параметр MaxImageDimension ограничивает максимальную сторону скриншота (например, до 1024px), сохраняя пропорции. При этом также происходит коррекция координат: при уменьшении картинки агент вычисляет коэффициент ScaleFactor. Модель видит сжатое изображение, но когда она возвращает координаты для клика, инструменты BrowserTools автоматически умножают их на этот коэффициент, обеспечивая корректное попадание в элемент на реальной странице. Это значение затем используется во всех инструментах, работающих с координатами (например, mouse_click). Стоит учитывать, что даже с учётом ScaleFactor, модель может возвращать неточные координаты. Поэтому рекомендуется использовать небольшие задержки и, при необходимости, повторные попытки клика.[/LIST]


Помимо MaxImageDimension, в агенте доступны дополнительные настройки, влияющие на передачу изображений в модель:

  • ScreenshotQuality — качество сжатия изображения (1–100).
    Чем ниже значение, тем меньше размер файла, но хуже детализация. По умолчанию: 70
  • ImageDetail — уровень детализации изображения, передаваемого в API.
    Допустимые значения: "original", "auto", "low", "high"
    По умолчанию: "original"
  • SendFullPageScreenshot — если true, снимается вся страница целиком, а не только видимый viewport.
    Полезно для длинных страниц, но увеличивает размер контекста

C#:
options.ScreenshotQuality = 60;
options.ImageDetail = "auto";
options.SendFullPageScreenshot = true;


5. Создание повторяемых сценариев

Одно из частых пожеланий при упоминании ИИ — научить его создавать кубики действий самостоятельно. Полноценно реализовать это как обычным пользователям не получится, но можно создать работающую альтернативу: записывать действия агента и воспроизводить их без участия ИИ.

Для этого в настройках есть флаг RecordMacro = true. При его включении в результате появляется result.Macro — записанная последовательность всех действий агента.

Макрос сохраняется и загружается в виде простого текста:

  • result.Macro.SaveAsText() — сохранить в строку (удобно для переменных ZennoPoster)
  • AgentMacro.LoadFromText(text) — загрузить обратно
  • .WithoutReadSteps() — убрать служебные шаги чтения DOM, которые при воспроизведении не нужны
  • .Slice(from, to) — взять только часть шагов

Пример того, как выглядит сохранённый макрос:

Код:
navigate url=http://lessons.zennolab.com/en/index
click by=xpath selector="//a[contains(., 'Simple registration')]" index=0
set_value find_by=xpath selector=//*[@id='email'] value=test@example.com use_selected_items=False index=0
set_value find_by=xpath selector=//*[@id='password'] value=MyPassword123 use_selected_items=False index=0
set_value find_by=xpath selector=//*[@id='password_repeat'] value=MyPassword123 use_selected_items=False index=0
click by=xpath selector="//input[@type='submit' and @value='Create']" index=0
wait ms=1000

Формат намеренно простой — при необходимости макрос можно отредактировать вручную прямо в переменной. Также доступен метод .Save() для сохранения в JSON — более читаемый формат для аудита, менее удобный для ручного редактирования.

C#:
bool macroExists = !string.IsNullOrEmpty(project.Variables["order_macro"].Value);

if (!macroExists)
{
    // Первый запуск: агент выполняет задачу и записывает макрос
    var options = new ZennoposterAiAgentOptions
    {
        ApiKey      = project.Variables["api_key"].Value,
        Model       = "gpt-5.4",
        RecordMacro = true,
    };
    options.SetMaxSteps("20");

    var agent  = new ZennoposterAiAgent(project, instance, options);
    var result = agent.Run(project.Variables["user_prompt"].Value);

    if (!result.Success)
    {
        project.SendErrorToLog("Агент не смог выполнить задачу: " + result.ErrorReason);
        throw new Exception(result.ErrorReason);
    }

    if (result.Macro != null)
    {
        var replayMacro = result.Macro.WithoutReadSteps();
        project.Variables["order_macro"].Value = replayMacro.SaveAsText();
        project.SendInfoToLog("Макрос записан, шагов: " + replayMacro.Steps.Count.ToString(), false);
    }
}
else
{
    // Последующие запуски: воспроизведение без API
    var macro      = AgentMacro.LoadFromText(project.Variables["order_macro"].Value);
    var player     = new AIAgentMacroPlayer(project, instance);
    var playResult = player.Play(macro);

    if (!playResult.Success)
    {
        project.SendWarningToLog("Воспроизведение не удалось: " + playResult.ErrorReason);
        // Сбрасываем — на следующем запуске агент запишет новый макрос
        project.Variables["order_macro"].Value = "";
        throw new Exception("Макрос устарел, будет перезаписан на следующем запуске.");
    }

    project.SendInfoToLog(
        "Макрос воспроизведён: " + playResult.StepsExecuted.ToString() +
        "/" + playResult.StepsTotal.ToString() + " шагов.", false);
}

Если воспроизведение упало на середине — можно продолжить с нужного шага:

C#:
// Продолжить с 5-го шага (нумерация с 0)
player.Play(macro, stopOnError: true, fromStep: 4);

// Или воспроизвести только часть макроса
var partial = macro.Slice(4, 10);
player.Play(partial);
В конце статьи прикреплен плагин для воспроизведения таким макросов, а также код этого плагина, чтобы запускать его напрямую. Стоит отметить что непосредственно c помощью плагина не будут работать функции работы с таблицами, переменными и так далее, так как это ограничения со стороны самого Zennoposter'a и плагина. Пример запуска будет в итоговом файле примере.

6. Устойчивость: autoRecovery и flex-режим

autoRecovery

Даже умные модели иногда ошибаются. Флаг AutoRecovery = true включает механизм восстановления: если выполнение инструмента завершилось ошибкой, агент отправляет модели описание ошибки и скриншот текущего состояния браузера, давая ей возможность попробовать другой подход.
Однако если вызван report_error, восстановление не происходит — выполнение завершается сразу.


  • AutoRecovery — включить восстановление после ошибок инструментов
  • MaxConsecutiveRecoveries — максимум последовательных восстановлений подряд (по умолчанию 3)

Важно: если модель сама вызвала report_error — это окончательная ошибка. Это означает, что ИИ не смог найти решение задачи, поэтому autoRecovery в таком случае не применяется.

flexMode

Flex service tier OpenAI — режим с пониженным приоритетом, дешевле примерно вдвое (на основании текущих тарифов на gpt-5.4), но с более высокой вероятностью таймаута или ошибки 429.

При FlexMode = true агент не падает сразу, а повторяет запрос с экспоненциальной задержкой:

5 с → 10 с → 20 с → ... (не более 60 с)

Если все попытки исчерпаны — автоматически переключается на стандартный приоритет и продолжает работу. Число повторов задаётся параметром MaxFlexRetries (по умолчанию 3).


7. Результат. Код для работы, плагин, примеры использования


Резюмируя, что может делать ИИ-агент:

  • Открывать страницы, очищать кеш, куки браузера
  • Читать содержимое страницы — текст, HTML, атрибуты конкретных элементов
  • Делать скриншоты и анализировать визуальное состояние страницы
  • Кликать, вводить текст, заполнять формы, выбирать значения в выпадающих списках (используя либо скриншоты, либо HTML самостоятельно принимая решения какой метод выбрать)
  • Взаимодействовать с переменными, списками и таблицами ZennoPoster, как для чтения так и для записи (не будет работать в режиме плагина)
  • Ждать загрузки динамического контента
  • Сообщать о невозможности выполнить задачу — если зашёл в тупик
  • Пытаться решить капчу (но нужно указывать напрямую в prompt что это нужно сделать, иначе чаще всего будет игнорирование)
  • Работать с разными движками браузеров - можно использовать как Chromium, Chrome, так и ZennoBrowser, главное их предварительно запусить, так как все работает все через одинаковые методы.

Агент адаптируется:

  • Если элемент не найден с первого раза — пробует другой селектор или подход
  • Если страница изменилась — перечитывает DOM и корректирует действия
  • При включённом AutoRecovery — анализирует ошибку по скриншоту и пробует снова

Что агент не умеет:

  • Гарантировать результат на любом сайте: различные изменения, сложное строение страниц, либо интерфейса может выдавать ошибки, но он будет пытаться.
  • Работать бесплатно — каждый шаг это запрос к API и расход токенов, разве что можно использовать локальные модели, но качество работы в таком случае заметно хуже.

В конце статьи я прикрепляю файл проекта в котором находится весь нужный исходный код. Никаких зависимостей извне не используется. Также отдельно прикреплены проекты плагинов как самого ИИ агента так и проигрывателя макросов, для его легкого изменения и компиляции, а также непосредственно сам плагин для добавления в проекты.​
Весь исходный код в проекте сопровождается комментариями, такой информации должно быть достаточно, чтобы при встраивании в проект было понятно что делает тот или иной параметр без необходимости подробно изучать реализацию, либо использовать ИИ для дальнейшей адаптации под свои нужды.​

Вариант 1 — использование напрямую в C# коде

Наиболее гибкий вариант. Полный доступ ко всем настройкам и результатам.

C#:
var options = new ZennoposterAiAgentOptions
{
    // --- Подключение к API ---
    ApiKey   = "sk-...",      // Bearer-токен. Обязательное поле
    Model    = "gpt-5.4",      // Идентификатор модели. Обязательное поле
    Proxy    = "host:port",   // Прокси. Пустая строка — без прокси
    Provider = new OpenAiCompatibleCompletionsProvider(
                   "https://api.deepseek.com/v1/chat/completions"
               ),             // По умолчанию OpenAiCompatibleResponsesProvider

    // --- Лимиты выполнения ---
    MaxSteps       = 10, // Макс. число шагов. -1 = без лимита, по умолчанию 5
    MaxTokensLimit = -1, // Макс. токенов. -1 = без лимита (по умолчанию)

    // --- Повторные попытки ---
    MaxRetries     = 1,  // Повторов при пустом ответе (не-flex). По умолчанию 1
    MaxFlexRetries = 3,  // Повторов в flex-режиме при таймауте / 429. По умолчанию 3

    // --- Автовосстановление ---
    AutoRecovery             = false, // Включить восстановление после ошибок инструментов
    MaxConsecutiveRecoveries = 3,     // Макс. восстановлений подряд. По умолчанию 3

    // --- Поведение модели ---
    SystemPrompt      = "You are a helpful assistant.", // Системный промпт
    FlexMode          = false, // service_tier=flex — дешевле, но медленнее
    ParallelToolCalls = false, // Параллельные вызовы инструментов за один шаг

    // --- Кеширование (только OpenAiCompatibleResponsesProvider) ---
    ExtendedCacheRetention = false,            // Хранить кеш 24 ч вместо стандартного TTL
    PromptCacheKey         = "my-project-v1", // Ключ кеша для переиспользования

    // --- Ценообразование (USD за 1 млн токенов, 0 = не считать) ---
    InputPricePerMillion       = 2.50m,
    CachedInputPricePerMillion = 0.63m,
    OutputPricePerMillion      = 10.0m,

    // --- Инструменты ---
    ToolGroups = ToolGroups.All, // Набор активных групп инструментов

    // --- Запись макроса ---
    RecordMacro = false, // Записывать последовательность действий в result.Macro

    // --- Логирование ---
    LogToZennoPoster = false, // Дублировать лог в интерфейс ZennoPoster

  // --- Начальный контекст страницы ---
    InitialContext = InitialContext.None,
    // Добавляет контекст текущей страницы к первому сообщению агента.
    // None       — без контекста (по умолчанию)
    // Screenshot — скриншот текущего вьюпорта
    // PageHtml   — очищенный HTML страницы (применяется HtmlCleanOptions)
    // DomText    — текстовое содержимое страницы (innerText)

    // --- Очистка HTML ---
    HtmlCleanOptions = new HtmlCleanOptions(), // настройки по умолчанию
    // Управляет тем, что удаляется из HTML перед передачей модели.
    // Влияет на get_page_html, get_element (html) и InitialContext.PageHtml/DomText.
    // Быстро: options.SetHtmlCleanPreset("aggressive").SetHtmlMaxLength("5000")
};

Поля, которые задаются из текстовых переменных проекта, устанавливаются через Set-методы — они принимают строку и сами разбирают значение. Некорректные или пустые строки молча игнорируются:

C#:
options
    .SetMaxSteps(project.Variables["max_steps"].Value)
    .SetMaxTokensLimit(project.Variables["token_limit"].Value)
    .SetPrices(
        project.Variables["input_price"].Value,   // "2.5" и "2,5" — оба варианта работают
        project.Variables["cached_price"].Value,
        project.Variables["output_price"].Value)
    .SetFlexMode(project.Variables["flex_mode"].Value)         // "true" / "1" / "да"
    .SetAutoRecovery(project.Variables["auto_recovery"].Value)
    .SetToolGroups(project.Variables["tool_groups"].Value);    // "Browser, DomReading"
   .SetInitialContext(project.Variables["initial_context"].Value)  // "None" / "Screenshot" / "PageHtml" / "DomText"
    .SetHtmlMaxLength(project.Variables["html_max_length"].Value)   // "8000" / "-1"
    .SetHtmlCleanPreset(project.Variables["html_preset"].Value)     // "Default" / "Minimal" / "Aggressive" / "None"

Результат выполнения — AgentRunResult

Метод agent.Run() возвращает объект AgentRunResult. Он содержит всё необходимое для обработки результата, логирования и отладки.

СвойствоТипОписание
SuccessboolTrue, если задача выполнена успешно
AnswerstringФинальный текстовый ответ модели. Заполняется только при Success = true
ErrorReasonstringПричина неудачи. Заполняется только при Success = false
TotalTokensintСуммарное число токенов за весь запуск
TotalInputTokensintВходные токены (включая кешированные)
TotalCachedInputTokensintИз них — кешированные входные токены
TotalOutputTokensintВыходные токены
EstimatedCostUsddecimalРасчётная стоимость в USD. Ноль, если цены не заданы в настройках
CostFormattedstringСтоимость в виде строки, например $0.004231
MacroAgentMacroЗаписанный макрос. Заполняется только при RecordMacro = true
DebugLogList<AgentDebugEntry>Подробный лог каждого шага: запрос, ответ и все вызовы инструментов



C#:
var result = agent.Run(project.Variables["user_prompt"].Value);

// Основной результат
if (!result.Success)
{
    project.SendErrorToLog("Ошибка: " + result.ErrorReason);
    throw new Exception(result.ErrorReason);
}
project.Variables["ai_answer"].Value = result.Answer;

// Токены и стоимость
project.Variables["total_tokens"].Value = result.TotalTokens.ToString();
project.Variables["ai_cost"].Value      = result.CostFormatted;

project.SendInfoToLog(string.Format(
    "Токены — вход: {0} (кеш: {1}), выход: {2}, всего: {3}",
    result.TotalInputTokens,
    result.TotalCachedInputTokens,
    result.TotalOutputTokens,
    result.TotalTokens), false);

// Макрос (если RecordMacro = true)
if (result.Macro != null)
{
    project.Variables["macro"].Value = result.Macro.WithoutReadSteps().SaveAsText();
}

// Полный трейс для отладки
project.Variables["debug_trace"].Value = result.GetDebugTrace();

Метод GetDebugTrace() возвращает текстовый дамп всех шагов: тело каждого запроса к API, сырой ответ модели, имена и аргументы вызванных инструментов и их результаты. Удобно сохранять в переменную и смотреть при возникновении проблем.​

C#:
var options = new ZennoposterAiAgentOptions
{
    ApiKey = project.Variables["api_key"].Value,
    Model  = "gpt-5.4",
};

var agent  = new ZennoposterAiAgent(project, instance, options);
var result = agent.Run(project.Variables["user_prompt"].Value);

if (!result.Success)
{
    project.SendErrorToLog("Ошибка: " + result.ErrorReason);
    throw new Exception(result.ErrorReason);
}

project.Variables["ai_answer"].Value = result.Answer;

C#:
var options = new ZennoposterAiAgentOptions
{
    ApiKey           = project.Variables["api_key"].Value,
    Model            = project.Variables["model"].Value,
    Proxy            = project.Variables["proxy"].Value,
    LogToZennoPoster = true,
    FlexMode         = true,
    AutoRecovery     = true,
    ToolGroups       = ToolGroups.All,
    PromptCacheKey   = "my-project-v1",
};
options
    .SetMaxSteps(project.Variables["max_steps"].Value)
    .SetPrices("2.50", "0.63", "10.0");

var agent  = new ZennoposterAiAgent(project, instance, options);
var result = agent.Run(project.Variables["user_prompt"].Value);

if (!result.Success)
{
    project.SendErrorToLog("Ошибка агента: " + result.ErrorReason);
    throw new Exception(result.ErrorReason);
}

project.Variables["ai_answer"].Value    = result.Answer;
project.Variables["total_tokens"].Value = result.TotalTokens.ToString();
project.Variables["ai_cost"].Value      = result.CostFormatted;

project.SendInfoToLog(string.Format(
    "Токены — вход: {0} (кеш: {1}), выход: {2}, всего: {3}",
    result.TotalInputTokens,
    result.TotalCachedInputTokens,
    result.TotalOutputTokens,
    result.TotalTokens), false);

project.SendInfoToLog("Стоимость: " + result.CostFormatted, false);

C#:
// DeepSeek
var options = new ZennoposterAiAgentOptions
{
    ApiKey   = project.Variables["deepseek_key"].Value,
    Model    = "deepseek-chat",
    Provider = new OpenAiCompatibleCompletionsProvider(
                   "https://api.deepseek.com/v1/chat/completions"),
};

// Локальная модель через LM Studio — ключ не нужен
var optionsLocal = new ZennoposterAiAgentOptions
{
    ApiKey   = "",
    Model    = "qwen/qwen3.5-9b",
    Provider = new OpenAiCompatibleCompletionsProvider(
                   "http://localhost:1234/v1/chat/completions"),
};
OpenAiCompatibleResponsesProvider используется по умолчанию — явно указывать его не нужно.

Хранение макроса можно вынести в файл или в любой другой удобный для проекта формат.
C#:
bool macroExists = !string.IsNullOrEmpty(project.Variables["order_macro"].Value);

if (!macroExists)
{
    var options = new ZennoposterAiAgentOptions
    {
        ApiKey      = project.Variables["api_key"].Value,
        Model       = "gpt-5.4",
        RecordMacro = true,
    };
    options.SetMaxSteps("20");

    var agent  = new ZennoposterAiAgent(project, instance, options);
    var result = agent.Run(project.Variables["user_prompt"].Value);

    if (!result.Success)
    {
        project.SendErrorToLog("Агент не смог выполнить задачу: " + result.ErrorReason);
        throw new Exception(result.ErrorReason);
    }

    if (result.Macro != null)
    {
        var replayMacro = result.Macro.WithoutReadSteps();
        project.Variables["order_macro"].Value = replayMacro.SaveAsText();
        project.SendInfoToLog("Макрос записан, шагов: " + replayMacro.Steps.Count.ToString(), false);
    }
}
else
{
    var macro      = AgentMacro.LoadFromText(project.Variables["order_macro"].Value);
    var player     = new AIAgentMacroPlayer(project, instance);
    var playResult = player.Play(macro);

    if (!playResult.Success)
    {
        project.SendWarningToLog("Воспроизведение не удалось: " + playResult.ErrorReason);
        project.Variables["order_macro"].Value = "";
        throw new Exception("Макрос устарел, будет перезаписан на следующем запуске.");
    }

    project.SendInfoToLog(
        "Макрос воспроизведён: " + playResult.StepsExecuted.ToString() +
        "/" + playResult.StepsTotal.ToString() + " шагов.", false);
}

При возникновении проблем можно получить полный лог каждого шага — запрос к API, ответ модели и результаты всех вызовов инструментов:
C#:
// Записать полный трейс в переменную для просмотра
project.Variables["debug_trace"].Value = result.GetDebugTrace();

// Или пройтись по шагам вручную
foreach (var entry in result.DebugLog)
{
    project.SendInfoToLog(entry.ToString(), false);
}

Для некоторых моделей, которые пока еще не заточены для работы с ИИ агентами свойственна проблема, связанная с тем что при отправке первого сообщения им нужно сразу подавать контекст работы, без которого они не запрашивают никаких действий со стороны программы (например такое свойственно для gpt5.4-nano).
Для этого у нас есть возможность прикрепить к первому сообщению контекст — скриншот или код страницы.
C#:
/ Перед вызовом Run браузер уже находится на нужной странице.
// InitialContext позволяет передать её состояние в первом сообщении,
// чтобы агент сразу понимал с чем работает.

// Вариант 1: скриншот — для визуально сложных страниц
var options = new ZennoposterAiAgentOptions
{
    ApiKey         = project.Variables["api_key"].Value,
    Model          = "gpt-5.4nano",
    InitialContext = InitialContext.Screenshot,
};

// Вариант 2: HTML — для страниц с богатой структурой DOM
var optionsHtml = new ZennoposterAiAgentOptions
{
    ApiKey         = project.Variables["api_key"].Value,
    Model          = "gpt-5.4nano",
    InitialContext = InitialContext.PageHtml,
    HtmlCleanOptions = HtmlCleanOptions.Aggressive, // сократить объём
};

// Вариант 3: текст — для простых текстовых страниц
var optionsText = new ZennoposterAiAgentOptions
{
    ApiKey         = project.Variables["api_key"].Value,
    Model          = "gpt-5.4nano",
    InitialContext = InitialContext.DomText,
};

// Установка из переменной проекта
options.SetInitialContext(project.Variables["initial_context"].Value);
// допустимые значения: "None", "Screenshot", "PageHtml", "DomText"

var agent  = new ZennoposterAiAgent(project, instance, options);
var result = agent.Run("Найди и нажми кнопку оформления заказа.");
Важно: Так как в текущем виде весь код находится в отдельном namespace, для того чтобы можно было его вызвать в вашем проекте после копирования этого кода, нужно также добавить using ZennoposterAiAgentCore; в "Директивы Using"​
Вариант 2 — использование как плагин

Настройки задаются в графическом интерфейсе кубика, ничего лишнего не подключается. Описание каждого параметра можно посмотреть в C# коде выше — специально дублировать подсказки в плагин не стал.

Ограничения плагина:
  • Нет доступа к переменным, спискам и таблицам ZennoPoster — используется ToolGroups.NoStorage
  • На выходе только финальный ответ модели и макрос для повторного выполнения.

Подробнее про плагины и как их устанавливать можно прочитать в документации:

За что отвечают конкретные настройки можно прочитать выше, либо воспользоваться поиском по документу.


141157
141158



На видео я продемонстрировал конкретные примеры ИИ агента на lessons.zennolab.com:


Проект открыт для изучения и доработки. Используйте на свой страх и риск — автор не берёт на себя ответственность за сбои в работе или расходы на API. В коде могут иметься ошибки и баги. Будьте внимательны при его использовании.​
 

Вложения

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

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