Files
deepseek-api/OLD_DEEPSHIM.md
T
Alex CubeandClaude Opus 5 5771817503 OLD_DEEPSHIM.md: заметки закрытого проекта
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>
2026-08-26 14:18:46 +03:00

285 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Наследство 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$="<id>"]` этого чата.
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`.