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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8c52cf101c
commit
5771817503
+284
@@ -0,0 +1,284 @@
|
|||||||
|
# Наследство 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`.
|
||||||
@@ -428,11 +428,11 @@ POST /api/chat
|
|||||||
|
|
||||||
Зависимостей нет — только то, что есть в Node 22.
|
Зависимостей нет — только то, что есть в Node 22.
|
||||||
|
|
||||||
Движок управления вкладкой перенесён из
|
Движок управления вкладкой перенесён из проекта DeepShim, который на этом
|
||||||
[DeepShim](https://git.08h.ru/alex/deepshim) и дальше живёт здесь своей жизнью.
|
и закрылся: своего в нём не осталось ничего. Всё, что удалось выяснить про
|
||||||
Там он часть большого инструмента с веб-пультом и разбором патчей; здесь нужен
|
разметку DeepSeek замерами, лежит рядом с кодом комментариями, а разбор
|
||||||
только чат. Всё, что удалось выяснить про разметку DeepSeek, перенесено вместе
|
«почему именно так» — в [OLD_DEEPSHIM.md](OLD_DEEPSHIM.md). Это знание дороже
|
||||||
с комментариями — это знание дороже кода.
|
кода: оно добывалось живыми прогонами и переоткрывается неделями.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user