DeepShim (ollama_orchestro) закрыт: движок браузера переехал сюда, веб-пульт с галочками и помощь с git — в deepwork, потребители ходят по HTTP, а Claude Code на локальной модели похоронен как неудачный. Своего в нём не осталось, зато осталось знание про веб-интерфейс DeepSeek, добытое живыми прогонами: почему признак конца ответа ищется в кнопке отправки, почему блоки кода пропадают у фонового окна, почему New chat нельзя жать из DOM, а кнопку редактирования — только из DOM, в каком порядке удаляется чат и на чём там уже ошибались. Почти всё это действует внутри browser.mjs и продублировано комментариями рядом с кодом. Файл — на случай, когда нужно понять ПОЧЕМУ, а комментария не хватило. Первым разделом — почему DeepShim больше не нужен, с тремя доводами, почему держать его дальше было бы вредно: дубликат знания разошёлся бы молча, две программы на одном профиле браузера удаляют чаты друг друга, потребителей не осталось. Намеренно не перенесено: заметки про бюджет контекста Claude Code, плагины и запуск оркестратора — они про мёртвую часть и к DeepSeek отношения не имеют. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
21 KiB
Наследство 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 |
командная строка ds "вопрос" |
не нужна: потребители ходят по HTTP, человек — curl |
Claude Code на локальной модели (start-orchestro) |
похоронен; его собственный README признавал, что на нескольких файлах модель с окном 32k срывается |
запуск браузера (browser-launch.ps1) |
deepseek-api поднимает браузер сам |
chats.json, init |
deepseek-api заводит чаты сам, при первом вопросе |
Три довода, почему держать его дальше было вредно, а не нейтрально:
- Дубликат знания. После переноса движок существовал в двух копиях. Правка вроде «кнопку New chat нельзя жать из DOM» (см. ниже) должна была бы делаться дважды, а сделалась бы один раз — и копии молча разошлись бы.
- Драка за сайдбар. Обе программы перед каждым вопросом чистят список
чатов от всего, чего нет в их собственном
chats.json. На общем профиле браузера они удаляют чаты друг друга — это не теория, так и случилось. - Потребителей не осталось.
alexcube_prews,emiliya_deepиdeepworkпереведены на HTTP кdeepseek-api26.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 чатах подряд:
- Навести мышь на строку чата настоящим
Input.dispatchMouseEvent. Без этого у неактивного чата кнопки...в DOM нет. У активного она видна всегда — из-за этого легко удалить не тот чат. - Только после наведения искать кнопку
...: ту, чей центр по вертикали лежит внутри прямоугольника ссылкиa[href$="<id>"]этого чата. - Клик по
...→ менюRename / Pin / Share / Delete. - Клик по
Delete→ диалог с кнопкамиCancelиDelete chat. Подпись лежит во вложенном элементе, поэтому фильтр «элемент без детей» её не находит — брать самый мелкий видимый элемент с нужным текстом. - Проверять результат по 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.