diff --git a/OLD_DEEPSHIM.md b/OLD_DEEPSHIM.md new file mode 100644 index 0000000..1fbcade --- /dev/null +++ b/OLD_DEEPSHIM.md @@ -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$=""]` этого чата. +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`. diff --git a/README.md b/README.md index 7422d0b..8a37e61 100644 --- a/README.md +++ b/README.md @@ -428,11 +428,11 @@ POST /api/chat Зависимостей нет — только то, что есть в Node 22. -Движок управления вкладкой перенесён из -[DeepShim](https://git.08h.ru/alex/deepshim) и дальше живёт здесь своей жизнью. -Там он часть большого инструмента с веб-пультом и разбором патчей; здесь нужен -только чат. Всё, что удалось выяснить про разметку DeepSeek, перенесено вместе -с комментариями — это знание дороже кода. +Движок управления вкладкой перенесён из проекта DeepShim, который на этом +и закрылся: своего в нём не осталось ничего. Всё, что удалось выяснить про +разметку DeepSeek замерами, лежит рядом с кодом комментариями, а разбор +«почему именно так» — в [OLD_DEEPSHIM.md](OLD_DEEPSHIM.md). Это знание дороже +кода: оно добывалось живыми прогонами и переоткрывается неделями. ---