Самодостаточность: движок браузера переехал внутрь, DeepShim больше не нужен
Раньше сервис был обёрткой: каждый запрос спавнил ollama_orchestro/ask.mjs дочерним процессом, гонял вопрос и ответ через временные файлы и читал чужой config.json ради профиля браузера. Работать это могло только рядом с установленным DeepShim. Теперь всё своё. browser.mjs — перенесённый из ask.mjs движок: CDP-сессия, помощники страницы со всем, что удалось выяснить про разметку DeepSeek, отправка с подтверждением, ожидание ответа по состоянию кнопки, работа с чатами. Пульт, разбор патчей и git остались в DeepShim, сюда не поехали. Побочно ушли дочерний процесс на каждый запрос и временные файлы: вопрос и ответ теперь просто переменные. Файл на диске остался только для картинки — <input type="file"> иначе не наполнить. Свои настройки вместо чужих: browserExe, profileDir и cdpPort в config.json, рядом config.local.json для путей конкретной машины (в репозиторий не идёт). Свой chats.json, тоже не в репозитории: это рабочее состояние, оно переписывается при каждой смене чата. Образец — chats.example.json. Добавлен GET /api/probe: что видно во вкладке прямо сейчас. ВАЖНО: профиль браузера должен быть отдельный. Перед каждым вопросом сайдбар подчищается от всего, чего нет в своём chats.json, поэтому две программы на одном профиле удаляют чаты друг друга. Проверено вживую, описано в README. Проверки: node test.mjs; сквозной прогон с нуля — свой чат заведён сам, ответ верный; пять правок в одном чате, на шестом запросе новый чат, старый удалён, сайдбар не растёт. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f5cf061fcc
commit
8c52cf101c
@@ -1,3 +1,9 @@
|
||||
node_modules/
|
||||
*.log
|
||||
|
||||
# Рабочее состояние, а не настройка: id живых чатов. Файл заводится сам
|
||||
# и переписывается при каждой смене чата. Образец — chats.example.json.
|
||||
chats.json
|
||||
|
||||
# Свой на каждой машине: пути к браузеру и профилю.
|
||||
config.local.json
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
Платить не нужно — используется ваш обычный аккаунт DeepSeek. Взамен всё
|
||||
медленно (секунды, а не миллисекунды) и работает только пока открыт браузер.
|
||||
|
||||
Программа самодостаточна: кроме Node и браузера ей ничего не требуется,
|
||||
внешних пакетов нет.
|
||||
|
||||
---
|
||||
|
||||
## Оглавление
|
||||
@@ -40,11 +43,10 @@
|
||||
|
||||
| Что | Зачем | Как проверить, что есть |
|
||||
|---|---|---|
|
||||
| **Windows** | скрипты запуска написаны под неё | — |
|
||||
| **Windows** | скрипт запуска написан под неё | — |
|
||||
| **Node.js 22 или новее** | на нём написана программа | `node --version` → должно быть `v22.…` и выше |
|
||||
| **Браузер на движке Chromium** | Яндекс.Браузер, Chrome, Edge — любой | — |
|
||||
| **Аккаунт DeepSeek** | бесплатный, регистрация на [chat.deepseek.com](https://chat.deepseek.com) | — |
|
||||
| **Проект DeepShim** | в нём живёт код, который умеет управлять вкладкой | ставится на шаге 1 |
|
||||
|
||||
Node.js, если его нет, ставится с [nodejs.org](https://nodejs.org) — берите
|
||||
версию LTS, кнопка слева. После установки **закройте и откройте заново**
|
||||
@@ -54,40 +56,27 @@ Node.js, если его нет, ставится с [nodejs.org](https://nodejs
|
||||
|
||||
## Установка по шагам
|
||||
|
||||
Всего шагов семь. Делайте по порядку, каждый заканчивается проверкой —
|
||||
Всего шагов пять. Делайте по порядку, каждый заканчивается проверкой —
|
||||
не переходите к следующему, пока предыдущий не дал ожидаемого результата.
|
||||
|
||||
### Шаг 1. Скачать обе программы
|
||||
|
||||
Нужны две: эта и DeepShim, в котором лежит код управления браузером.
|
||||
Положите их **рядом, в одну папку**:
|
||||
|
||||
```
|
||||
C:\#Projects\
|
||||
deepseek-api\ <- эта программа
|
||||
ollama_orchestro\ <- DeepShim
|
||||
```
|
||||
### Шаг 1. Скачать
|
||||
|
||||
```cmd
|
||||
cd C:\#Projects
|
||||
git clone https://git.08h.ru/alex/deepseek-api.git
|
||||
git clone https://git.08h.ru/alex/deepshim.git ollama_orchestro
|
||||
```
|
||||
|
||||
Папка DeepShim должна называться именно `ollama_orchestro` — так она указана
|
||||
в настройках по умолчанию. Хотите иначе — поправите путь на шаге 6.
|
||||
Без git можно скачать ZIP с той же страницы и распаковать. Папка может лежать
|
||||
где угодно.
|
||||
|
||||
Без git можно просто скачать ZIP с тех же страниц и распаковать.
|
||||
|
||||
> **Проверка:** в `C:\#Projects\deepseek-api\` лежит `server.mjs`,
|
||||
> в `C:\#Projects\ollama_orchestro\` — `ask.mjs`.
|
||||
> **Проверка:** в папке есть `server.mjs`, `browser.mjs` и `start.cmd`.
|
||||
|
||||
### Шаг 2. Завести отдельный профиль браузера
|
||||
|
||||
Браузером будет управлять программа, поэтому работать в нём вам не нужно —
|
||||
и не стоит: он будет самопроизвольно открываться и переключать вкладки.
|
||||
Заведите под него **отдельный профиль** — это просто пустая папка,
|
||||
браузер сам её заполнит:
|
||||
и не стоит: он будет сам открываться, переключать вкладки и удалять чаты.
|
||||
Заведите под него **отдельный профиль** — это просто пустая папка, браузер
|
||||
сам её заполнит:
|
||||
|
||||
```cmd
|
||||
mkdir C:\Browsers\deepseek
|
||||
@@ -95,20 +84,22 @@ mkdir C:\Browsers\deepseek
|
||||
|
||||
Имя любое, запомните путь — понадобится на следующем шаге.
|
||||
|
||||
### Шаг 3. Настроить DeepShim
|
||||
> **Профиль должен быть свой.** Если в том же профиле работает другая
|
||||
> программа, управляющая чатами DeepSeek (например,
|
||||
> [DeepShim](https://git.08h.ru/alex/deepshim)), они начнут удалять чаты друг
|
||||
> друга: каждая считает лишним всё, чего нет в её собственном списке.
|
||||
|
||||
В папке `ollama_orchestro` создайте файл `config.json`:
|
||||
### Шаг 3. Указать путь к браузеру и профилю
|
||||
|
||||
Откройте `config.json` и поправьте два значения:
|
||||
|
||||
```json
|
||||
{
|
||||
"profileDir": "C:\\Browsers\\deepseek",
|
||||
"browserExe": "C:\\Program Files\\Yandex\\YandexBrowser\\Application\\browser.exe",
|
||||
"port": 9333
|
||||
"profileDir": "C:\\Browsers\\deepseek"
|
||||
}
|
||||
```
|
||||
|
||||
Три значения:
|
||||
|
||||
- **`profileDir`** — папка из шага 2.
|
||||
- **`browserExe`** — путь к браузеру. Типичные:
|
||||
|
||||
@@ -118,82 +109,20 @@ mkdir C:\Browsers\deepseek
|
||||
| Google Chrome | `C:\Program Files\Google\Chrome\Application\chrome.exe` |
|
||||
| Microsoft Edge | `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe` |
|
||||
|
||||
- **`port`** — порт, по которому программа управляет браузером. `9333` подходит,
|
||||
меняйте только если он у вас занят.
|
||||
|
||||
**Обратные слэши в JSON удваиваются** — `C:\\Browsers\\deepseek`, а не
|
||||
`C:\Browsers\deepseek`. С одинарными файл не прочитается.
|
||||
|
||||
> **Проверка:** откройте `config.json` в браузере (перетащите файл в окно).
|
||||
> Если показалась структура — JSON верный. Если ошибка — ищите пропущенную
|
||||
> запятую или одинарный слэш.
|
||||
> Чтобы обновление программы не затирало ваши пути, их можно положить
|
||||
> не в `config.json`, а в `config.local.json` рядом — он перебивает основной
|
||||
> и в репозиторий не попадает.
|
||||
|
||||
### Шаг 4. Запустить браузер и войти в DeepSeek
|
||||
> **Проверка:** перетащите `config.json` в окно браузера. Показалась
|
||||
> структура — JSON верный. Ошибка — ищите пропущенную запятую или одинарный
|
||||
> слэш.
|
||||
|
||||
### Шаг 4. Запустить
|
||||
|
||||
```cmd
|
||||
cd C:\#Projects\ollama_orchestro
|
||||
start-browser.cmd
|
||||
```
|
||||
|
||||
Откроется браузер с чистым профилем на странице DeepSeek. **Войдите
|
||||
в аккаунт.** Логин сохранится в профиле — больше делать это не придётся.
|
||||
|
||||
> **Проверка:** в окне командной строки написано
|
||||
> `READY. … is listening on port 9333.`
|
||||
> Если написано `Port 9333 did not answer` — см. [Если не работает](#если-не-работает).
|
||||
|
||||
**Окно браузера не сворачивайте и не закрывайте.** Свёрнутое окно Windows
|
||||
не отрисовывает, и программа не сможет прочитать ответ. Достаточно перевести
|
||||
его на другой рабочий стол или просто оставить позади других окон.
|
||||
|
||||
### Шаг 5. Создать три рабочих чата
|
||||
|
||||
DeepSeek выбирает режим (обычный / с поиском в интернете / со зрением)
|
||||
**в момент создания чата**, и потом переключить его нельзя — тумблеры
|
||||
исчезают после первого сообщения. Поэтому под каждый режим нужен свой чат.
|
||||
|
||||
Создадутся они сами:
|
||||
|
||||
```cmd
|
||||
cd C:\#Projects\ollama_orchestro
|
||||
node ask.mjs init
|
||||
```
|
||||
|
||||
Команда откроет три новых чата, задаст в каждом по вопросу и запишет их
|
||||
номера в `chats.json`. Займёт минуту-две, окно браузера в это время будет
|
||||
само щёлкать — так и надо, не мешайте.
|
||||
|
||||
> **Проверка:** в конце напечатается что-то вроде
|
||||
> ```
|
||||
> Сохранено в chats.json:
|
||||
> think expert af016e20-…
|
||||
> search default+… 8021bbc1-…
|
||||
> vision vision 14663b36-…
|
||||
> ```
|
||||
|
||||
### Шаг 6. Настроить deepseek-api
|
||||
|
||||
Файл `config.json` в папке `deepseek-api` уже готов:
|
||||
|
||||
```json
|
||||
{
|
||||
"port": 11500,
|
||||
"host": "0.0.0.0",
|
||||
"bridge": "C:/#Projects/ollama_orchestro/ask.mjs",
|
||||
"chatUrl": "https://chat.deepseek.com/",
|
||||
"defaultRole": "think",
|
||||
"timeout": 300
|
||||
}
|
||||
```
|
||||
|
||||
Если вы разложили папки как в шаге 1 — **менять здесь нечего, переходите
|
||||
к шагу 7.** Если DeepShim лежит в другом месте, поправьте `bridge`. Здесь
|
||||
слэши обычные, прямые (`C:/…`), а не обратные.
|
||||
|
||||
### Шаг 7. Запустить
|
||||
|
||||
```cmd
|
||||
cd C:\#Projects\deepseek-api
|
||||
start.cmd
|
||||
```
|
||||
|
||||
@@ -206,26 +135,34 @@ start.cmd
|
||||
Это окно должно оставаться открытым — пока оно живо, работает и сервис.
|
||||
Останов — `Ctrl+C` или просто закрыть окно.
|
||||
|
||||
**Готово.** Дальше — проверка.
|
||||
### Шаг 5. Войти в DeepSeek
|
||||
|
||||
---
|
||||
|
||||
## Проверка, что всё работает
|
||||
|
||||
Откройте **вторую** командную строку (первая занята сервисом) и спросите
|
||||
что-нибудь:
|
||||
Браузер поднимается сам, но в пустом профиле вы ещё не залогинены. Дайте
|
||||
сервису первый запрос — он откроет окно:
|
||||
|
||||
```cmd
|
||||
curl http://localhost:11500/api/tags
|
||||
```
|
||||
|
||||
Ожидается список из трёх «моделей»:
|
||||
Первый вызов ответит `503` и поднимет браузер. **Войдите в аккаунт DeepSeek
|
||||
в открывшемся окне.** Логин сохранится в профиле — больше делать это
|
||||
не придётся.
|
||||
|
||||
```json
|
||||
{"models":[{"name":"deepseek:think",…},{"name":"deepseek:search",…},{"name":"deepseek:vision",…}]}
|
||||
```
|
||||
**Окно браузера не сворачивайте и не закрывайте.** Свёрнутое окно Windows
|
||||
не отрисовывает, и программа не сможет прочитать ответ. Достаточно перевести
|
||||
его на другой рабочий стол или оставить позади других окон.
|
||||
|
||||
Теперь настоящий вопрос:
|
||||
> **Проверка:** повторите `curl http://localhost:11500/api/tags` — теперь
|
||||
> должен прийти список из трёх «моделей».
|
||||
|
||||
**Готово.** Чаты под каждый режим программа заведёт сама при первом же
|
||||
вопросе — создавать их руками не нужно.
|
||||
|
||||
---
|
||||
|
||||
## Проверка, что всё работает
|
||||
|
||||
Откройте **вторую** командную строку (первая занята сервисом):
|
||||
|
||||
```cmd
|
||||
curl http://localhost:11500/api/chat -d "{\"model\":\"deepseek:think\",\"stream\":false,\"messages\":[{\"role\":\"user\",\"content\":\"Ответь одним словом: столица Японии?\"}]}"
|
||||
@@ -237,13 +174,25 @@ curl http://localhost:11500/api/chat -d "{\"model\":\"deepseek:think\",\"stream\
|
||||
{"model":"deepseek:think","created_at":"…","message":{"role":"assistant","content":"Токио"},"done":true,"done_reason":"stop"}
|
||||
```
|
||||
|
||||
Если пришло — всё работает. В окне сервиса при этом видно, сколько заняло:
|
||||
В окне сервиса при этом видно, что происходило:
|
||||
|
||||
```
|
||||
[think] 37 симв.
|
||||
[think] ответ 5 симв. за 8 с
|
||||
чат think не найден, создаю сразу с вопросом
|
||||
открыт новый чат
|
||||
создан новый чат think: 660347d5-…
|
||||
[think] ответ 5 симв. за 9 с
|
||||
```
|
||||
|
||||
Отдельная диагностика — что сейчас видно во вкладке:
|
||||
|
||||
```cmd
|
||||
curl http://localhost:11500/api/probe
|
||||
```
|
||||
|
||||
Покажет адрес вкладки, найдено ли поле ввода, идёт ли генерация, список чатов
|
||||
и не просит ли сайт залогиниться.
|
||||
|
||||
---
|
||||
|
||||
## Как подключить свою программу
|
||||
@@ -257,8 +206,9 @@ http://localhost:11500
|
||||
Моделью выберите `deepseek:think`.
|
||||
|
||||
Если программа работает на **другом компьютере** в вашей сети — вместо
|
||||
`localhost` укажите адрес этого компьютера, например `http://192.168.0.10:11500`.
|
||||
Сервис слушает все адреса, ничего дополнительно настраивать не нужно.
|
||||
`localhost` укажите адрес этого компьютера, например
|
||||
`http://192.168.0.10:11500`. Сервис слушает все адреса, ничего дополнительно
|
||||
настраивать не нужно.
|
||||
|
||||
### Три «модели» — это три режима
|
||||
|
||||
@@ -268,9 +218,9 @@ http://localhost:11500
|
||||
| `deepseek:search` | с поиском в интернете | нужны свежие данные |
|
||||
| `deepseek:vision` | со зрением | разбор картинок |
|
||||
|
||||
Картинку можно просто приложить к запросу, как это делает Ollama —
|
||||
в поле `images` массивом base64. Тогда режим `vision` включится сам,
|
||||
независимо от того, какую модель вы указали.
|
||||
Картинку можно приложить к запросу так же, как это делает Ollama — в поле
|
||||
`images` массивом base64. Тогда режим `vision` включится сам, независимо
|
||||
от того, какую модель вы указали.
|
||||
|
||||
**Незнакомое имя модели — не ошибка.** Если программа настойчиво шлёт свою
|
||||
`llama3` или `qwen3:8b`, запрос уйдёт в обычный чат. Это сделано нарочно:
|
||||
@@ -292,6 +242,7 @@ http://localhost:11500
|
||||
|---|---|
|
||||
| `GET /api/tags` | список «моделей». **Отвечает 503, пока браузер не поднят** — по этому клиенты понимают, что сервис не готов |
|
||||
| `GET /api/version` | опознавательная строка |
|
||||
| `GET /api/probe` | сверх протокола Ollama: что видно во вкладке прямо сейчас |
|
||||
| `POST /api/chat` | сообщения → ответ |
|
||||
| `POST /api/generate` | то же, но вопрос одной строкой |
|
||||
| `POST /api/embeddings` | **501** — веб-чат векторов не отдаёт |
|
||||
@@ -314,11 +265,12 @@ http://localhost:11500
|
||||
|
||||
### Сколько ждать
|
||||
|
||||
Порядок величин, замерено на разборе фразы и генерации тегов:
|
||||
Порядок величин, замерено на коротких вопросах:
|
||||
|
||||
| | Ollama (qwen3:8b, локально) | DeepSeek (веб) |
|
||||
|---|---|---|
|
||||
| короткий вопрос | 2–3 с | 8–12 с |
|
||||
| короткий вопрос | 2–3 с | 7–12 с |
|
||||
| запрос, на котором меняется чат | — | ~16 с |
|
||||
| первый запрос после запуска браузера | — | +15 с на подъём окна |
|
||||
|
||||
Если ваша программа считает, что модель обязана ответить за пару секунд, —
|
||||
@@ -334,7 +286,9 @@ http://localhost:11500
|
||||
|---|---|---|
|
||||
| `port` | `11500` | порт, на котором сервис слушает |
|
||||
| `host` | `0.0.0.0` | какие адреса слушать; `127.0.0.1` — только этот компьютер |
|
||||
| `bridge` | `C:/#Projects/ollama_orchestro/ask.mjs` | путь к DeepShim |
|
||||
| `cdpPort` | `9333` | порт, по которому программа управляет браузером |
|
||||
| `browserExe` | Яндекс.Браузер | путь к браузеру |
|
||||
| `profileDir` | `C:\Browsers\deepseek` | папка профиля браузера |
|
||||
| `chatUrl` | `https://chat.deepseek.com/` | что открывать во вкладке |
|
||||
| `defaultRole` | `think` | режим для незнакомых имён моделей |
|
||||
| `timeout` | `300` | сколько секунд ждать **бездействия** страницы |
|
||||
@@ -342,9 +296,12 @@ http://localhost:11500
|
||||
Про `timeout`: это не «сколько ждать ответа». Пока DeepSeek печатает, ждём
|
||||
сколько угодно; срок отсчитывается от последнего признака жизни страницы.
|
||||
|
||||
`config.local.json` рядом перебивает `config.json` и в репозиторий не
|
||||
попадает — туда удобно класть пути конкретной машины.
|
||||
|
||||
### Переменные окружения
|
||||
|
||||
Перебивают файл, удобны для разовых запусков:
|
||||
Перебивают файлы, удобны для разовых запусков:
|
||||
|
||||
```cmd
|
||||
set DEEPSEEK_API_PORT=11501
|
||||
@@ -352,9 +309,10 @@ set DEEPSEEK_CDP_PORT=9444
|
||||
start.cmd
|
||||
```
|
||||
|
||||
Профиль браузера, путь к нему и порт управления берутся из
|
||||
`ollama_orchestro/config.json` — там уже выполнен вход в DeepSeek, и второй
|
||||
экземпляр этих настроек однажды разошёлся бы с первым.
|
||||
### `chats.json`
|
||||
|
||||
Заводится сам, руками его трогать не нужно — см.
|
||||
[Чаты изнашиваются](#чаты-изнашиваются-и-меняются-сами).
|
||||
|
||||
---
|
||||
|
||||
@@ -365,29 +323,21 @@ start.cmd
|
||||
| Что видно | В чём дело | Что делать |
|
||||
|---|---|---|
|
||||
| `Браузер с отладочным портом 9333 не отвечает` в ответе на `/api/tags` | браузер закрыт | Ничего: он поднимется сам при первом запросе к `/api/chat`. Если не поднялся — следующая строка |
|
||||
| `Браузер так и не открыл отладочный порт` | этот профиль уже открыт **без** порта управления: новый процесс отдаёт ссылку старому окну и молча завершается | Закройте **все** окна браузера с этим профилем и повторите. Проверить, что не осталось: диспетчер задач, процессы браузера |
|
||||
| `в config.json не задан profileDir или browserExe` | шаг 3 пропущен или файл лежит не в той папке | `ollama_orchestro\config.json`, оба ключа заполнены |
|
||||
| `мост вышел с кодом 1. Вкладка chat.deepseek.com не открыта` | вкладку закрыли руками | Откройте `chat.deepseek.com` в том же окне браузера, или перезапустите его через `start-browser.cmd` |
|
||||
| `Браузер так и не открыл отладочный порт` | этот профиль уже открыт **без** порта управления: новый процесс отдаёт ссылку старому окну и молча завершается | Закройте **все** окна браузера с этим профилем и повторите. Проверить, что не осталось: диспетчер задач |
|
||||
| `Браузер не найден: …` | неверный `browserExe` | Проверьте путь и удвоенные слэши |
|
||||
| `не задан browserExe или profileDir` | шаг 3 пропущен | Заполните оба ключа |
|
||||
| `Вкладка chat.deepseek.com не открыта` | вкладку закрыли руками | Сервис откроет её сам на следующем запросе; если нет — откройте `chat.deepseek.com` в том же окне |
|
||||
| Ответ приходит **обрезанным**, без кода | окно браузера свёрнуто — Windows перестаёт его отрисовывать, а DeepSeek дорисовывает блоки кода лениво | Разверните окно. Не сворачивайте: уводите на другой рабочий стол или за другие окна |
|
||||
| Ответ приходит **чужой**, не на ваш вопрос | вкладка не успела обновиться | Повторите запрос. Если повторяется — перезапустите браузер |
|
||||
| В окне мелькнуло `правки в этом чате кончились, завожу новый` | исчерпан лимит в шесть правок одного сообщения | Ничего. Это штатная смена чата, см. [Чаты изнашиваются](#чаты-изнашиваются-и-меняются-сами) |
|
||||
| В логе `правки в этом чате кончились, завожу новый` | исчерпан лимит в шесть правок одного сообщения | Ничего. Это штатная смена чата, см. [Чаты изнашиваются](#чаты-изнашиваются-и-меняются-сами) |
|
||||
| Чаты пропадают сами, хотя вы их не удаляли | тот же профиль браузера использует другая программа, управляющая чатами | Дайте каждой свой `profileDir` |
|
||||
| `EADDRINUSE: address already in use` | сервис уже запущен в другом окне | Закройте то окно либо возьмите другой порт (`DEEPSEEK_API_PORT`) |
|
||||
| `node не является внутренней или внешней командой` | Node не установлен или командная строка открыта до его установки | Поставьте Node, **закройте и откройте заново** командную строку |
|
||||
| Просит войти в DeepSeek при каждом запуске | профиль браузера каждый раз новый | Проверьте, что `profileDir` указывает на одну и ту же существующую папку |
|
||||
|
||||
Отдельная диагностика самого моста — из папки DeepShim:
|
||||
|
||||
```cmd
|
||||
node ask.mjs probe
|
||||
```
|
||||
|
||||
Она скажет, видна ли вкладка, найдено ли поле ввода и не просит ли сайт
|
||||
залогиниться.
|
||||
| Просит войти в DeepSeek при каждом запуске | профиль каждый раз новый | Проверьте, что `profileDir` указывает на одну и ту же существующую папку |
|
||||
|
||||
Проверить перевод формата, не поднимая браузер:
|
||||
|
||||
```cmd
|
||||
cd C:\#Projects\deepseek-api
|
||||
node test.mjs
|
||||
```
|
||||
|
||||
@@ -426,12 +376,10 @@ POST /api/chat
|
||||
├─ выбрать чат по имени модели (think / search / vision)
|
||||
├─ встать в очередь (вкладка одна)
|
||||
│
|
||||
├─> написать вопрос во временный файл
|
||||
├─> node ask.mjs ask --file … --out … (дочерний процесс)
|
||||
│ └─> Chrome DevTools Protocol
|
||||
├─> поднять браузер, если он не отвечает
|
||||
├─> Chrome DevTools Protocol
|
||||
│ └─> правка ПЕРВОГО сообщения чата
|
||||
│ └─> дождаться, пока DeepSeek допечатает
|
||||
├─< прочитать ответ из файла
|
||||
│
|
||||
└─ отдать в формате Ollama
|
||||
```
|
||||
@@ -441,46 +389,51 @@ POST /api/chat
|
||||
Контекст не растёт, мусор между запросами не копится, каждый вопрос —
|
||||
с чистого листа.
|
||||
|
||||
Почему дочерний процесс, а не импорт: `ask.mjs` — программа командной строки,
|
||||
она ничего не экспортирует и выполняется сразу при подключении. Переделывать
|
||||
чужой проект ради этого не стали.
|
||||
|
||||
Почему браузер поднимается напрямую, а не через `browser-launch.ps1`
|
||||
из DeepShim: тот скрипт рассчитан на человека за консолью — на занятом профиле
|
||||
он задаёт вопрос и ждёт ответа, а у сервиса некому отвечать.
|
||||
Почему браузер запускается напрямую из программы, а не батником: батник
|
||||
рассчитан на человека за консолью — на занятом профиле он задаёт вопрос
|
||||
и ждёт ответа, а у сервиса некому отвечать.
|
||||
|
||||
### Чаты изнашиваются и меняются сами
|
||||
|
||||
**Одно сообщение DeepSeek разрешает править ровно шесть раз.** После шестой
|
||||
правки кнопка перестаёт открывать поле ввода — и чат становится непригоден.
|
||||
|
||||
Ничего делать не нужно, это уже предусмотрено. На седьмом вопросе DeepShim
|
||||
Ничего делать не нужно, это предусмотрено. На седьмом вопросе программа
|
||||
заводит новый чат того же режима, задаёт вопрос сразу в нём, записывает новый
|
||||
номер в `chats.json` и **удаляет отработавший чат**. Снаружи не видно ничего:
|
||||
тот же запрос, тот же ответ, разве что на несколько секунд дольше.
|
||||
|
||||
Отсюда два следствия:
|
||||
Отсюда следствия:
|
||||
|
||||
- **`chats.json` — не настройка, а переменная.** Номера в нём меняются сами
|
||||
по мере работы. Не правьте его руками и не удивляйтесь, если git покажет
|
||||
в нём изменения.
|
||||
по мере работы. Не правьте его руками; в репозиторий он не попадает,
|
||||
образец структуры — `chats.example.json`.
|
||||
- **В сайдбаре DeepSeek не копится мусор.** Старые чаты удаляются, плюс перед
|
||||
каждым вопросом сайдбар подчищается от посторонних служебных чатов.
|
||||
|
||||
Если вы удалите рабочий чат вручную — тоже ничего страшного: следующий запрос
|
||||
каждым вопросом сайдбар подчищается от всего, чего нет в `chats.json`.
|
||||
Именно поэтому профиль браузера должен быть свой: чужие чаты в нём
|
||||
программа считает мусором.
|
||||
- Если вы удалите рабочий чат вручную — ничего страшного: следующий запрос
|
||||
заметит пропажу и создаст новый.
|
||||
|
||||
Файлы:
|
||||
### Файлы
|
||||
|
||||
| Файл | Что в нём |
|
||||
|---|---|
|
||||
| `server.mjs` | всё: HTTP, очередь, запуск браузера, вызов моста |
|
||||
| `config.json` | настройки этой машины |
|
||||
| `server.mjs` | HTTP, перевод формата Ollama, очередь |
|
||||
| `browser.mjs` | всё про браузер: CDP, разметка DeepSeek, отправка, ожидание ответа, чаты |
|
||||
| `config.json` | настройки; `config.local.json` рядом перебивает их |
|
||||
| `chats.json` | рабочее состояние: id живых чатов (заводится сам) |
|
||||
| `start.cmd` | запуск |
|
||||
| `test.mjs` | проверка перевода формата, браузер не нужен |
|
||||
|
||||
Зависимостей нет — только то, что есть в Node 22.
|
||||
|
||||
Движок управления вкладкой перенесён из
|
||||
[DeepShim](https://git.08h.ru/alex/deepshim) и дальше живёт здесь своей жизнью.
|
||||
Там он часть большого инструмента с веб-пультом и разбором патчей; здесь нужен
|
||||
только чат. Всё, что удалось выяснить про разметку DeepSeek, перенесено вместе
|
||||
с комментариями — это знание дороже кода.
|
||||
|
||||
---
|
||||
|
||||
## Лицензия
|
||||
|
||||
+1128
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"think": {
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"mode": "expert"
|
||||
},
|
||||
"search": {
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"mode": "default+Search"
|
||||
},
|
||||
"vision": {
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"mode": "vision"
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"port": 11500,
|
||||
"host": "0.0.0.0",
|
||||
"bridge": "C:/#Projects/ollama_orchestro/ask.mjs",
|
||||
"cdpPort": 9333,
|
||||
"browserExe": "C:\\Program Files\\Yandex\\YandexBrowser\\Application\\browser.exe",
|
||||
"profileDir": "C:\\Browsers\\deepseek",
|
||||
"chatUrl": "https://chat.deepseek.com/",
|
||||
"defaultRole": "think",
|
||||
"timeout": 300
|
||||
|
||||
+33
-125
@@ -6,146 +6,43 @@
|
||||
// и отдаёт наружу протокол Ollama: /api/tags и /api/chat. Любая программа,
|
||||
// которая уже умеет ходить в Ollama, подключается сюда сменой одного адреса.
|
||||
//
|
||||
// Что внутри происходит: запрос → файл → мост ollama_orchestro/ask.mjs →
|
||||
// правка первого сообщения в чате DeepSeek → ответ → файл → HTTP.
|
||||
// Здесь только HTTP и перевод формата. Всё, что касается браузера и разметки
|
||||
// DeepSeek, живёт в browser.mjs.
|
||||
//
|
||||
// Зависимостей нет: Node 22+ со встроенным fetch.
|
||||
// Зависимостей нет: Node 22+ со встроенными fetch и WebSocket.
|
||||
|
||||
import { createServer } from 'node:http';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { readFile, writeFile, rm, mkdtemp } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
// fileURLToPath, а не разбор pathname вручную: путь проекта содержит «#»,
|
||||
import { configure, ask, probe, browserAlive, ROLES } from './browser.mjs';
|
||||
|
||||
// fileURLToPath, а не разбор pathname вручную: путь проекта может содержать «#»,
|
||||
// и в URL он приезжает как %23 — самодельный разбор ломается именно на нём.
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const cfg = {
|
||||
port: 11500,
|
||||
host: '0.0.0.0',
|
||||
bridge: 'C:/#Projects/ollama_orchestro/ask.mjs',
|
||||
cdpPort: 9333,
|
||||
browserExe: '',
|
||||
profileDir: '',
|
||||
chatUrl: 'https://chat.deepseek.com/',
|
||||
defaultRole: 'think',
|
||||
timeout: 300,
|
||||
...JSON.parse(await readFile(join(HERE, 'config.json'), 'utf8').catch(() => '{}')),
|
||||
// Личные пути — сюда: этот файл в репозиторий не попадает, и обновление
|
||||
// проекта не затрёт настройки конкретной машины.
|
||||
...JSON.parse(await readFile(join(HERE, 'config.local.json'), 'utf8').catch(() => '{}')),
|
||||
};
|
||||
if (process.env.DEEPSEEK_API_PORT) cfg.port = Number(process.env.DEEPSEEK_API_PORT);
|
||||
if (process.env.DEEPSEEK_CDP_PORT) cfg.cdpPort = Number(process.env.DEEPSEEK_CDP_PORT);
|
||||
|
||||
// Отладочный порт браузера живёт в конфиге моста: он один на все проекты,
|
||||
// и держать его копию здесь значит однажды разойтись.
|
||||
const bridgeDir = dirname(cfg.bridge);
|
||||
const bridgeCfg = JSON.parse(
|
||||
await readFile(join(bridgeDir, 'config.json'), 'utf8').catch(() => '{}'));
|
||||
const cdpPort = Number(process.env.DEEPSEEK_CDP_PORT || bridgeCfg.port || 9333);
|
||||
configure(cfg);
|
||||
|
||||
// Роли — это отдельные чаты DeepSeek под разные режимы (обычный, с поиском,
|
||||
// со зрением). Режим выбирается при создании чата и потом не меняется, поэтому
|
||||
// чатов несколько. Снаружи они выглядят как разные «модели» Ollama.
|
||||
const ROLES = ['think', 'search', 'vision'];
|
||||
const MODELS = ROLES.map(r => 'deepseek:' + r);
|
||||
|
||||
// ------------------------------------------------------------------ браузер
|
||||
|
||||
const cdp = path => fetch(`http://127.0.0.1:${cdpPort}${path}`,
|
||||
{ signal: AbortSignal.timeout(3000) });
|
||||
|
||||
async function browserAlive() {
|
||||
try {
|
||||
return (await cdp('/json/version')).ok;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
// Флаги не украшение. Замер в ollama_orchestro 11.08.2026: у неактивного окна
|
||||
// Windows сообщает браузеру, что оно перекрыто, рисование прекращается, а
|
||||
// DeepSeek рисует блоки кода лениво — ответ приезжает без кода (3136 символов
|
||||
// вместо 30 817). Эти флаги запрещают браузеру засыпать.
|
||||
const BROWSER_FLAGS = [
|
||||
'--remote-allow-origins=*',
|
||||
'--start-maximized',
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-features=CalculateNativeWinOcclusion',
|
||||
];
|
||||
|
||||
// Поднять браузер, если он не отвечает, и открыть вкладку с чатом, если её нет.
|
||||
// Профиль и путь к браузеру берём из конфига моста — там уже выполнен вход
|
||||
// в DeepSeek, а второй профиль означал бы второй логин.
|
||||
//
|
||||
// Браузер запускается напрямую, а не через browser-launch.ps1 из ollama_orchestro:
|
||||
// тот скрипт рассчитан на человека за консолью — на занятом профиле он
|
||||
// спрашивает Read-Host, а у сервиса некому отвечать.
|
||||
async function ensureBrowser() {
|
||||
if (!(await browserAlive())) {
|
||||
if (!bridgeCfg.profileDir || !bridgeCfg.browserExe) {
|
||||
throw new Error(
|
||||
`Браузер не отвечает на порту ${cdpPort}, и запустить его сам не могу: ` +
|
||||
`в ${join(bridgeDir, 'config.json')} не задан profileDir или browserExe.`);
|
||||
}
|
||||
console.log('Браузер не отвечает, поднимаю...');
|
||||
spawn(bridgeCfg.browserExe,
|
||||
[`--user-data-dir=${bridgeCfg.profileDir}`,
|
||||
`--remote-debugging-port=${cdpPort}`,
|
||||
...BROWSER_FLAGS, cfg.chatUrl],
|
||||
{ detached: true, stdio: 'ignore' }).unref();
|
||||
// Холодный профиль поднимается неспешно; 60 секунд с запасом.
|
||||
for (let i = 0; i < 30 && !(await browserAlive()); i++) await sleep(2000);
|
||||
if (!(await browserAlive())) {
|
||||
throw new Error(
|
||||
`Браузер так и не открыл отладочный порт ${cdpPort}. Скорее всего этот ` +
|
||||
`профиль уже открыт БЕЗ отладочного порта: новый процесс отдаёт ссылку ` +
|
||||
`старому окну и завершается. Закройте окно с профилем ` +
|
||||
`${bridgeCfg.profileDir} и повторите.`);
|
||||
}
|
||||
}
|
||||
|
||||
const list = await (await cdp('/json/list')).json().catch(() => []);
|
||||
if (list.some(t => String(t.url || '').includes('deepseek'))) return;
|
||||
// Новая вкладка в свежих сборках Chromium открывается ТОЛЬКО методом PUT:
|
||||
// на GET протокол отвечает 405 и молча ничего не делает.
|
||||
const r = await fetch(
|
||||
`http://127.0.0.1:${cdpPort}/json/new?url=${encodeURIComponent(cfg.chatUrl)}`,
|
||||
{ method: 'PUT' });
|
||||
if (!r.ok) throw new Error(`Не удалось открыть вкладку DeepSeek: ${r.status}.`);
|
||||
await sleep(4000); // дать чату прогрузиться до первого вопроса
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ мост
|
||||
|
||||
// Вопрос и ответ ездят файлами, а не через stdout: вопрос бывает на десяток
|
||||
// килобайт, ответ — на сотню, и труба тут только мешает.
|
||||
async function askBridge(prompt, role, imageBytes) {
|
||||
await ensureBrowser();
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsapi-'));
|
||||
const qFile = join(dir, 'q.txt'), aFile = join(dir, 'a.txt');
|
||||
const iFile = join(dir, 'image.jpg');
|
||||
try {
|
||||
await writeFile(qFile, prompt, 'utf8');
|
||||
const args = ['ask', '--file', qFile, '--out', aFile, '--quiet', '--no-seed',
|
||||
'--chat', role, '--timeout', String(cfg.timeout)];
|
||||
if (imageBytes) {
|
||||
await writeFile(iFile, imageBytes);
|
||||
args.push('--image', iFile);
|
||||
}
|
||||
await new Promise((resolve, reject) => {
|
||||
const ch = spawn(process.execPath, [cfg.bridge, ...args],
|
||||
{ cwd: bridgeDir, stdio: ['ignore', 'ignore', 'pipe'] });
|
||||
let err = '';
|
||||
ch.stderr.on('data', d => { err += d; });
|
||||
ch.on('error', reject);
|
||||
ch.on('close', c => (c === 0 ? resolve() : reject(new Error(
|
||||
`мост вышел с кодом ${c}. ${err.trim().slice(0, 300)}`))));
|
||||
});
|
||||
return await readFile(aFile, 'utf8');
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Вкладка одна, и два запроса разом затрут друг другу поле ввода. Поэтому
|
||||
// очередь: запросы идут строго по одному.
|
||||
// ponytail: очередь без ограничения длины — если клиент завалит сервис
|
||||
@@ -221,7 +118,7 @@ const server = createServer(async (req, res) => {
|
||||
if (!(await browserAlive())) {
|
||||
return json(res, 503, {
|
||||
models: [],
|
||||
error: `Браузер с отладочным портом ${cdpPort} не отвечает. ` +
|
||||
error: `Браузер с отладочным портом ${cfg.cdpPort} не отвечает. ` +
|
||||
'Он поднимется сам при первом запросе к /api/chat; если этого ' +
|
||||
'не произошло — профиль, скорее всего, уже открыт без ' +
|
||||
'отладочного порта, закройте то окно.',
|
||||
@@ -239,15 +136,23 @@ const server = createServer(async (req, res) => {
|
||||
return json(res, 200, { version: 'deepseek-api' });
|
||||
}
|
||||
|
||||
// Своё, сверх протокола Ollama: что видно во вкладке прямо сейчас.
|
||||
if (req.method === 'GET' && url.pathname === '/api/probe') {
|
||||
return json(res, 200, await serial(() => probe()));
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && url.pathname === '/api/chat') {
|
||||
const body = await readBody(req);
|
||||
const prompt = flatten(body.messages);
|
||||
if (!prompt) return json(res, 400, { error: 'пустой запрос' });
|
||||
const image = imageOf(body.messages);
|
||||
const role = image ? 'vision' : roleOf(body.model);
|
||||
const imageBytes = imageOf(body.messages);
|
||||
const role = imageBytes ? 'vision' : roleOf(body.model);
|
||||
const started = Date.now();
|
||||
console.log(`[${role}] ${prompt.length} симв.${image ? ' + картинка' : ''}`);
|
||||
const answer = await serial(() => askBridge(prompt, role, image));
|
||||
console.log(`[${role}] ${prompt.length} симв.${imageBytes ? ' + картинка' : ''}`);
|
||||
const answer = await serial(() => ask({
|
||||
prompt, role, imageBytes, timeout: cfg.timeout,
|
||||
log: m => console.log(` ${m}`),
|
||||
}));
|
||||
console.log(`[${role}] ответ ${answer.length} симв. за ${
|
||||
Math.round((Date.now() - started) / 1000)} с`);
|
||||
const model = body.model || ('deepseek:' + role);
|
||||
@@ -268,7 +173,9 @@ const server = createServer(async (req, res) => {
|
||||
const prompt = [body.system, body.prompt].filter(Boolean).join('\n\n').trim();
|
||||
if (!prompt) return json(res, 400, { error: 'пустой запрос' });
|
||||
const role = roleOf(body.model);
|
||||
const answer = await serial(() => askBridge(prompt, role, null));
|
||||
const answer = await serial(() => ask({
|
||||
prompt, role, timeout: cfg.timeout, log: m => console.log(` ${m}`),
|
||||
}));
|
||||
const head = { model: body.model || ('deepseek:' + role), created_at: now() };
|
||||
if (body.stream === false) {
|
||||
return json(res, 200, { ...head, response: answer, done: true, done_reason: 'stop' });
|
||||
@@ -296,6 +203,7 @@ const server = createServer(async (req, res) => {
|
||||
// По умолчанию слушаем все адреса: подключаются и с этой машины, и с соседней —
|
||||
// ровно как к самой Ollama. Сервис ходит в браузер под вашим логином, поэтому
|
||||
// в недоверенной сети host в config.json стоит сузить до 127.0.0.1.
|
||||
//
|
||||
// Запускаемся только когда файл вызвали напрямую: перевод формата проверяется
|
||||
// из test.mjs импортом, и поднимать ради этого сервер незачем.
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
@@ -303,7 +211,7 @@ server.listen(cfg.port, cfg.host, () => {
|
||||
console.log(`DeepSeek прикидывается Ollama: http://localhost:${cfg.port}`);
|
||||
if (cfg.host !== '0.0.0.0') console.log(`Слушаем только ${cfg.host}`);
|
||||
console.log(`Модели: ${MODELS.join(', ')}`);
|
||||
console.log(`Мост: ${cfg.bridge} (браузер на порту ${cdpPort})`);
|
||||
console.log(`Браузер: порт ${cfg.cdpPort}, профиль ${cfg.profileDir || '(не задан)'}`);
|
||||
console.log('Остановить — Ctrl+C');
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user