Проверено вживую: браузер убит через 10 с после начала запроса. Сокет умирал
честно, браузер поднимался, но повтор падал на «кнопка New chat не найдена» —
ensureBrowser возвращался, едва отладочный порт отозвался. У холодного
браузера адрес задан аргументом командной строки, поэтому вкладка есть
в списке сразу, а разметки нет ещё несколько секунд, и ветка с ожиданием
после /json/new в этом случае не выполнялась вовсе.
Теперь ensureBrowser в обоих случаях ждёт поле ввода — примету того, что
страницей можно пользоваться, — и отдельно распознаёт страницу входа, где
ждать нечего. После правки тот же опыт даёт полный ответ: запрос пережил
смерть браузера и вернул 9888 символов за 140 с.
DevTools берёт всё, что стоит после '?' в /json/new, как сам адрес целиком —
это не параметры запроса. Строка 'url=https%3A%2F%2Fchat.deepseek.com%2F'
честно считалась адресом, и открывалась about:blank. Место срабатывает
только когда браузер жив, а вкладки с чатом нет, поэтому лежало незамеченным.
Заодно ensureBrowser теперь проверяет, что открылось именно то: беззвучный
промах в этом файле уже был один раз, с кнопкой New chat.
Лог обрывался на «Браузер не отвечает, поднимаю...» и дальше молчал даже
при успехе — со стороны неотличимо от зависшего сервиса. Теперь ensureBrowser
говорит, что браузер поднялся и что вкладка открыта.
Заодно окно в трее больше не советует Ctrl+C: консоли там нет, останов —
«Выход» в меню значка.
Три вещи, и первая - настоящая дыра, а не удобство.
1. Смерть вкладки посреди запроса вешала сервис насмерть. У Session не было
ни обработчика close, ни таймаута на вызов: закрылась вкладка в момент
ожидания - обещание не разрешалось НИКОГДА. Запрос висел, очередь за ним
стояла, сервис при этом выглядел живым и молчал. Теперь close отклоняет
всё висящее, у каждого вызова протокола потолок 60 с, у подключения 10 с,
а мёртвый сокет отказывает сразу, а не копит вызовы.
2. ask() повторяет запрос один раз, если вкладка пропала посреди работы.
Повтор безопасен именно из-за правки первого сообщения: вопрос не
добавляется к чату, а заменяет его содержимое, поэтому дважды он
не задастся.
3. Сторож: раз в 30 с проверяет браузер и вкладку и поднимает, не дожидаясь
вопроса. Раньше восстановление жило внутри запроса, и клиент платил за него
15-60 секундами своего времени. Ходит через ТУ ЖЕ очередь, что и запросы,
иначе полез бы в браузер, пока DeepSeek дописывает ответ. Пропускает свой
черёд, если запрос уже идёт. При неудаче отсрочка удваивается до 300 с:
обычная причина (профиль открыт другим окном без отладочного порта) сама
не лечится, и долбиться каждые полминуты значит плодить процессы.
Выключается ключом watchdog в config.json.
Добавлен GET /api/state - дешёвый снимок для индикаторов: браузер, готовность,
занятость, длина очереди, секунды текущего запроса, последняя ошибка и время
последней проверки. Вкладку не трогает, спрашивать можно хоть раз в секунду.
ВАЖНО для будущего окна с индикаторами: снимок обновляет сторож, поэтому он
устаревает на срок до watchdogSec - смотреть на поле checked, а не только
на ready.
Проверено вживую: сервис поднялся, /api/state отдал ready; браузер убит дважды
- в логе "сторож: Браузер не отвечает, поднимаю...", checked идёт каждые 30 с,
состояние возвращается в ready.
НЕ проверено вживую: восстановление после смерти вкладки ПОСРЕДИ запроса
(пункты 1 и 2). Код компилируется, разбор формата проходит, но подгадать
момент не пробовал.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Раньше сервис был обёрткой: каждый запрос спавнил ollama_orchestro/ask.mjs
дочерним процессом, гонял вопрос и ответ через временные файлы и читал
чужой config.json ради профиля браузера. Работать это могло только рядом
с установленным DeepShim.
Теперь всё своё. browser.mjs — перенесённый из ask.mjs движок: CDP-сессия,
помощники страницы со всем, что удалось выяснить про разметку DeepSeek,
отправка с подтверждением, ожидание ответа по состоянию кнопки, работа
с чатами. Пульт, разбор патчей и git остались в DeepShim, сюда не поехали.
Побочно ушли дочерний процесс на каждый запрос и временные файлы: вопрос
и ответ теперь просто переменные. Файл на диске остался только для картинки —
<input type="file"> иначе не наполнить.
Свои настройки вместо чужих: browserExe, profileDir и cdpPort в config.json,
рядом config.local.json для путей конкретной машины (в репозиторий не идёт).
Свой chats.json, тоже не в репозитории: это рабочее состояние, оно
переписывается при каждой смене чата. Образец — chats.example.json.
Добавлен GET /api/probe: что видно во вкладке прямо сейчас.
ВАЖНО: профиль браузера должен быть отдельный. Перед каждым вопросом сайдбар
подчищается от всего, чего нет в своём chats.json, поэтому две программы
на одном профиле удаляют чаты друг друга. Проверено вживую, описано в README.
Проверки: node test.mjs; сквозной прогон с нуля — свой чат заведён сам,
ответ верный; пять правок в одном чате, на шестом запросе новый чат, старый
удалён, сайдбар не растёт.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>