# Наследство DeepShim DeepShim (`ollama_orchestro`) — проект, из которого вырос `deepseek-api`. Он закрыт 26.08.2026, репозиторий и папка удалены. Здесь лежит то, что переживает код: **добытое замерами знание про веб-интерфейс DeepSeek**. Почти всё это уже действует внутри `browser.mjs` и продублировано там комментариями рядом с кодом. Этот файл — на случай, когда нужно понять **почему** что-то сделано именно так, а комментария рядом не хватило. --- ## Почему DeepShim больше не нужен DeepShim был прокладкой между человеком и веб-чатом: мост `ask.mjs` умел говорить с вкладкой браузера, а поверх него жили командная строка, веб-пульт и Claude Code на локальной модели, поручающий думание DeepSeek. К августу 2026 от него не осталось ничего своего: | Часть DeepShim | Куда делась | |---|---| | движок браузера (851 строка `ask.mjs`) | перенесён в `deepseek-api/browser.mjs` | | веб-пульт с галочками, патчи, помощь с git | перенесены в [deepwork](https://git.08h.ru/alex/deepwork) | | командная строка `ds "вопрос"` | не нужна: потребители ходят по HTTP, человек — `curl` | | Claude Code на локальной модели (`start-orchestro`) | похоронен; его собственный README признавал, что на нескольких файлах модель с окном 32k срывается | | запуск браузера (`browser-launch.ps1`) | `deepseek-api` поднимает браузер сам | | `chats.json`, `init` | `deepseek-api` заводит чаты сам, при первом вопросе | Три довода, почему держать его дальше было вредно, а не нейтрально: 1. **Дубликат знания.** После переноса движок существовал в двух копиях. Правка вроде «кнопку New chat нельзя жать из DOM» (см. ниже) должна была бы делаться дважды, а сделалась бы один раз — и копии молча разошлись бы. 2. **Драка за сайдбар.** Обе программы перед каждым вопросом чистят список чатов от всего, чего нет в их собственном `chats.json`. На общем профиле браузера они удаляют чаты друг друга — это не теория, так и случилось. 3. **Потребителей не осталось.** `alexcube_prews`, `emiliya_deep` и `deepwork` переведены на HTTP к `deepseek-api` 26.08.2026. Что **не** перенесено сюда намеренно: заметки про бюджет контекста Claude Code, плагины, `~/.claude.json` и запуск оркестратора. Они были про мёртвую часть проекта и к веб-чату DeepSeek отношения не имеют. --- ## Устройство страницы DeepSeek Проверено, не догадки: - Модель — radiogroup `div[data-model-type="default|expert|vision"][role="radio"]`, выбранная помечена `aria-checked="true"`. Читать только отсюда. - `Search` (иконка глобуса) — **отдельный тумблер**. Без него модель не ищет в сети. - `DeepThink` — НЕ модель, а показ размышлений. Не трогать. - Тумблеры и выбор модели доступны только в **пустом** чате. Отсюда и берётся правило «под каждый режим свой чат»: после первого сообщения переключить нельзя. - Кнопка отправки — самая правая круглая `div[role="button"].ds-button` под полем ввода. Подписи и aria нет, класс генерится; ищется по модификатору `ds-button--primary` (на странице ровно один), геометрия — запасной путь. ### Список сообщений виртуализован Единственный `data-`атрибут на всей странице — `data-virtual-list-item-key`. Вопрос отличается от ответа тем, что внутри ответа есть `.ds-markdown`. **За номер ключа цепляться нельзя.** После правки список перенумеровывается: было `1`/`2`, стало `3`/`4`. Вдобавок виртуализация выбрасывает из DOM всё, что не на экране, поэтому «элемент с ключом 1» может не существовать вовсе. Первая версия кода ловила именно `key="1"` и падала со второго вопроса. Рабочий признак: среди отрисованных элементов взять с наименьшим ключом тот, внутри которого **нет** `.ds-markdown`. --- ## Правка первого сообщения У сообщения пользователя есть кнопка редактирования — **вторая** из двух кнопок под сообщением (первая — «копировать»). Клик открывает `textarea` с исходным текстом. Если править первое сообщение и отправлять заново, DeepSeek отбрасывает всё, что было после: чат остаётся из одного обмена, контекст не растёт. Две особенности, обе проверены: - Кнопки под сообщением появляются по наведению и **прячутся, когда курсор уходит с сообщения вниз к ним**. Поэтому настоящий мышиный клик по ним не срабатывает — нужен DOM-вызов `element.click()` после наведения на само сообщение. Это единственное место, где DOM-клик уместен (ср. с New chat ниже). - Координаты кнопок брать нельзя: разметка съезжает по мере роста ответа. **Правок ровно ШЕСТЬ.** После шестой DeepSeek перестаёт открывать поле правки: кнопка нажимается, `textarea` не появляется. Чат при этом расходник — заводится новый той же роли, прежний удаляется. --- ## Кнопку New chat нельзя жать из DOM (26.08.2026) `btn.click()` React глотает молча: адрес страницы не менялся, а функция отчитывалась успехом. Дальше код считывал **старый** id как новый, ничего не удалял, и вопрос уходил обычным сообщением в тот же чат. Снаружи выглядело как «ротация не работает»: за десять запросов чат не сменился ни разу, зато разросся до трёх обменов. Замер обоих способов на живой странице: | Способ | Адрес после | |---|---| | `btn.click()` из DOM | не изменился | | настоящие мышиные события | стал `null` — открыт пустой чат | **Общее правило, которое отсюда следует:** любая функция, которая «сделала действие» на странице, обязана подтверждать результат наблюдением. Беззвучный отказ React выглядит как успех, и ловится потом неделями. --- ## Когда ответ готов Признаком конца ответа было «текст не менялся 2,5 с» — и это врало. В режиме expert DeepSeek думает паузами, любая заминка длиннее порога выглядела как конец. Замер на одном ответе: наружу ушло **640** символов, тогда как ответ дорос до **4135** — терялось шесть седьмых, в первую очередь блоки кода. Дефект был молчаливый: ошибки нет, ответ выглядит нормальным. Надёжный признак нашёлся в кнопке отправки. Пока DeepSeek печатает, она превращается в «стоп» и остаётся активной, хотя поле ввода уже пусто: | Состояние | Класс кнопки | Поле ввода | |---|---|---| | простой | есть `ds-button--disabled` | пусто | | печатает | нет `ds-button--disabled` | пусто | | готов отправить | нет `ds-button--disabled` | есть текст | Проверено: класс `--disabled` пропал на 3-й секунде ответа и вернулся на 42-й, ровно когда текст перестал расти. --- ## Обрезанные ответы: две настоящие причины ### 1. Браузер в фоне — блоков кода в DOM нет вообще DeepSeek рисует блоки кода лениво. У неактивного окна страница не отрисовывается, и содержимое блока в DOM просто не создаётся: остаются первые две строки-заглушки. Достать остальное нельзя ничем — ни `innerText`, ни `textContent`, данных нет. Прокрутка не помогает: проход по всему ответу шагами не дал ни одного лишнего символа. Помогает единственное — заставить браузер отрисовать страницу, а через CDP это делает `Page.captureScreenshot` (jpeg, качество 1; картинка выбрасывается). | ответ | до отрисовки | после | |---|---|---| | TaskQueue | 2 068 | 24 362 | | EventBus | 3 136 | 30 817 | | Router | 258 | 32 045 | Флаги запуска браузера (`--start-maximized`, `--disable-backgrounding-occluded-windows`, `--disable-renderer-backgrounding`, `--disable-background-timer-throttling`, `--disable-features=CalculateNativeWinOcclusion`) **не отменяют** скриншот: свёрнутое окно Windows не рисует вообще. Последний флаг — про Windows: система сообщает браузеру, что окно перекрыто, и рисование прекращается. ### 2. Кнопка Continue DeepSeek режет длинный ответ по своему лимиту и предлагает дописать. Генерация при этом честно завершается, кнопка отправки гаснет — и это выглядит как конец. Текст продолжения уходит **в тот же блок ответа** (32 073 → 46 422 символа, число блоков не изменилось), склеивать ничего не нужно: нажать и ждать дальше. Разметка: `SPAN.ds-button__content` с текстом `Continue` внутри `DIV.ds-button.ds-button--outlinedNeutral`. Три ловушки, каждая стоила попытки: - **искать только среди `.ds-button`**, а не по тексту где угодно. Подсветка синтаксиса оборачивает ключевое слово `continue` в отдельный `span` с ровно таким текстом. Первая версия кликала по слову в коде десять раз подряд. Ответ с JavaScript почти всегда содержит такое слово — случай обычный; - **селектор строго `.ds-button`**, не `[class*="ds-button"]` — маска цепляет вложенный `ds-button__content`, и клик уходит мимо: обработчик на кнопке; - если всё же искать по тексту — брать самый **глубокий** элемент: внешний `div.ds-flex` тоже содержит слово Continue, клик по нему не делает ничего. `element.click()` тут не работает — нужен настоящий мышиный клик по координатам, после `scrollIntoView` и с паузой. Сквозная проверка: ответ на 47 414 символов, класс и тесты целые. --- ## Фоновое окно ломает не только текст, но и клики Тот же корень вылез второй раз — в удалении чатов. Код находил строку чата, но кнопку «…» не находил никогда: уборка молча возвращала «удалено: 0» при шестнадцати лишних чатах. Кнопка появляется по `:hover`, а **у окна в фоне рисование стоит** — DOM-узла просто не создаётся, сколько мышь ни наводи. Лечится тем же `Page.captureScreenshot` после каждого действия, которое должно что-то показать: наведение на строку, открытие меню, диалог подтверждения. После правки 15 лишних чатов удалены за один проход. **Правило:** если элемент виден человеку, но не находится из кода — первым делом принудительная отрисовка, и только потом искать ошибку в селекторе. --- ## Удаление чата Порядок строго такой, проверено на 12 чатах подряд: 1. Навести мышь на строку чата настоящим `Input.dispatchMouseEvent`. Без этого у неактивного чата кнопки `...` в DOM нет. У **активного** она видна всегда — из-за этого легко удалить не тот чат. 2. Только после наведения искать кнопку `...`: ту, чей центр по вертикали лежит внутри прямоугольника ссылки `a[href$=""]` этого чата. 3. Клик по `...` → меню `Rename / Pin / Share / Delete`. 4. Клик по `Delete` → диалог с кнопками `Cancel` и `Delete chat`. Подпись лежит во вложенном элементе, поэтому фильтр «элемент без детей» её не находит — брать самый **мелкий** видимый элемент с нужным текстом. 5. Проверять результат по id, а не по названию. **Адресоваться только по id.** Порядок чатов в сайдбаре меняется: тот, куда писали последним, встаёт первым. Индексы и запомненные координаты протухают между шагами. Ошибка на этом уже случалась: удалился не тот чат, потому что поиск кнопки поднимался до общего контейнера и брал самую правую видимую кнопку — а ею оказалась кнопка активного чата. --- ## Кнопка Retry: «Server is busy» DeepSeek иногда отвечает `Server is busy. Try again later, or use Instant Mode.` и рисует круглую оранжевую кнопку ↻ слева от сообщения. Генерация при этом не идёт, и ожидание досиживает до таймаута впустую. Опознаётся по модификатору `ds-button--warning` — на странице он только у неё (подписи и aria нет). Жать до пяти раз с паузой 3 с, проверяя **до** условия стабильности: текста может не быть вовсе. Потребители подстрахованы на случай, когда повторы исчерпаны и текст про занятость всё же доехал: они распознают его и отдают понятную ошибку, а не принимают за ответ модели. --- ## Мелочи, каждая стоила отладки - **Отступы теряются без тройных кавычек.** Содержимое между `=== FILE: ... ===` рендерится как обычный текст, и разметка съедает ведущие пробелы — первый прогон вернул JavaScript без единого отступа. Файл обязан лежать внутри ``` ``` ```. Страница показывает блок кода без самих кавычек, но подпись языка («text», «javascript») остаётся отдельной первой строкой — её надо отбрасывать. - **Вставка текста в поле — только нативным сеттером** плюс событие `input`. Иначе React не заметит изменения и отправит пустое сообщение. - **Отправка подтверждается.** `Enter` срабатывает не всегда: текст оставался в поле, а код уходил ждать ответ, которого не будет. Проверять, что поле опустело, иначе жать саму кнопку. - **Новая вкладка через CDP открывается только методом PUT.** На GET протокол отвечает 405 и молча ничего не делает. - **Тело POST-запроса склеивать только `Buffer.concat`.** Складывание кусков как строк рвёт многобайтовые символы на границе — русский вопрос доезжал до DeepSeek крякозябрами. - **Ответы поискового чата приходят с мусором от сносок:** цифры ссылок («-5-8») попадают в текст отдельными строками. Чистить при извлечении. - **Профиль браузера, открытый БЕЗ отладочного порта, не переоткрыть.** Новый процесс отдаёт ссылку старому окну и завершается, порт так и не появляется. Лечится только закрытием всех окон этого профиля. --- ## Что осталось недоделанным и умерло вместе с проектом - Переименование чатов (пункт `Rename` в том же меню) не автоматизировано — нужды не возникло. - Второй чат-сжиматель, куда уходил бы длинный ответ с просьбой ужать до сути. Задумывался как запасной путь против переполнения контекста локальной модели; вместе с ней и потерял смысл. - Двойники путей в `~/.claude.json` (`C:/#Сканы` и `c:/#Сканы`) не вычищены: правку блокирует классификатор разрешений. К DeepSeek отношения не имеет, записано на случай, если однажды всплывёт. Резервная копия тех настроек — `~\.claude.json.bak-rtk-20260806`.