diff --git a/README.md b/README.md index 00c5ee9..6664238 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # FreeDeepseekAPI

- Локальный OpenAI-compatible API proxy для DeepSeek Web Chat + Local OpenAI-compatible API proxy for DeepSeek Web Chat

@@ -12,38 +12,38 @@

- Быстрый старт • - Возможности • - Примеры • - Модели • + Quick Start • + Features • + Examples • + ModelsEndpointsOpen WebUI

-FreeDeepseekAPI поднимает локальный API-сервер для **DeepSeek Web Chat** (`chat.deepseek.com`) и позволяет подключать DeepSeek Web к Open WebUI, LiteLLM, Hermes, Claude Code, OpenAI SDK-style клиентам и другим OpenAI-compatible инструментам. +FreeDeepseekAPI runs a local API server for **DeepSeek Web Chat** (`chat.deepseek.com`) and lets you connect DeepSeek Web to Open WebUI, LiteLLM, Hermes, Claude Code, OpenAI SDK-style clients, and other OpenAI-compatible tools. -Проект работает через ваш обычный залогиненный аккаунт DeepSeek в отдельном Chrome-профиле. Локальный сервер принимает API-запросы, а дальше сам ходит в DeepSeek Web через сохранённую browser-сессию. +The project works through your regular logged-in DeepSeek account in a separate Chrome profile. The local server accepts API requests and then talks to DeepSeek Web through the saved browser session. -> ⚠️ Это экспериментальный web-chat proxy. DeepSeek может менять внутренний Web API без предупреждения. Для production-кейсов надёжнее официальный платный API DeepSeek. +> ⚠️ This is an experimental web-chat proxy. DeepSeek may change the internal Web API without warning. For production use cases, the official paid DeepSeek API is more reliable. ForgetMeAI: https://t.me/forgetmeai --- -## Навигация +## Navigation -- [Что это даёт](#-что-это-даёт) -- [Возможности](#-возможности) -- [Быстрый старт](#-быстрый-старт) -- [Windows запуск](#-windows-запуск) -- [Linux / Chromium запуск](#-linux--chromium-запуск) -- [VPS / headless запуск](#-vps--headless-запуск) +- [What this gives you](#-what-this-gives-you) +- [Features](#-features) +- [Quick Start](#-quick-start) +- [Windows launch](#-windows-launch) +- [Linux / Chromium launch](#-linux--chromium-launch) +- [VPS / headless launch](#-vps--headless-launch) - [Diagnostics / doctor](#-diagnostics--doctor) -- [Session reuse и сброс чатов](#-session-reuse-и-сброс-чатов) +- [Session reuse and chat reset](#-session-reuse-and-chat-reset) - [Multi-account pool](#-multi-account-pool) -- [Идеи для консольной авторизации](#-идеи-для-консольной-авторизации) -- [Проверка работы](#-проверка-работы) -- [Примеры запросов](#-примеры-запросов) +- [Console auth ideas](#-console-auth-ideas) +- [Verify it works](#-verify-it-works) +- [Request examples](#-request-examples) - [Chat Completions](#chat-completions) - [Reasoning](#reasoning) - [Web search](#web-search) @@ -51,40 +51,44 @@ ForgetMeAI: https://t.me/forgetmeai - [Anthropic Messages API](#anthropic-messages-api) - [OpenAI Responses API](#openai-responses-api) - [Tool calling](#tool-calling) -- [Модели](#-модели) +- [Models](#-models) - [Endpoints](#-endpoints) - [Open WebUI](#-open-webui) -- [Обновить логин](#-обновить-логин) -- [Статус проекта](#-статус-проекта) +- [Chrome Extension](#-chrome-extension) +- [Dashboard Web](#-dashboard-web) +- [Update login](#-update-login) +- [Project status](#-project-status) --- -## ✨ Что это даёт +## ✨ What this gives you -- Использовать DeepSeek Web как локальный API endpoint. -- Подключать DeepSeek к Open WebUI и другим OpenAI-compatible клиентам. -- Получать обычные JSON-ответы или streaming SSE. -- Использовать reasoning-модели с отдельным `reasoning_content`. -- Работать с Anthropic Messages API shim для Claude Code / Anthropic SDK. -- Использовать OpenAI Responses API shim для новых OpenAI/Codex-style клиентов. -- Держать отдельные web-сессии для разных агентов/users. +- Use DeepSeek Web as a local API endpoint. +- Connect DeepSeek to Open WebUI and other OpenAI-compatible clients. +- Get regular JSON responses or streaming SSE. +- Use reasoning models with separate `reasoning_content`. +- Work with Anthropic Messages API shim for Claude Code / Anthropic SDK. +- Use OpenAI Responses API shim for new OpenAI/Codex-style clients. +- Keep separate web sessions for different agents/users. -## 🚀 Возможности +## Features - **OpenAI-compatible API:** `POST /v1/chat/completions` - **Anthropic-compatible shim:** `POST /v1/messages` - **OpenAI Responses shim:** `POST /v1/responses` -- **Streaming:** SSE chunks и обычные non-stream JSON-ответы -- **Reasoning output:** отдельный `reasoning_content` для thinking-моделей -- **Tool calling:** парсинг OpenAI tools, Anthropic tools и Responses function tools -- **Model capabilities:** `GET /v1/model-capabilities` с alias → real web mode -- **Agent sessions:** отдельная DeepSeek-сессия на `user` / agent id -- **Session recovery:** авто-сброс устаревших chains/sessions -- **Zero dependencies:** Node.js 18+, без npm-зависимостей +- **Streaming:** SSE chunks and regular non-stream JSON responses +- **Reasoning output:** separate `reasoning_content` for thinking models +- **Tool calling:** parsing OpenAI tools, Anthropic tools, and Responses function tools +- **Model capabilities:** `GET /v1/model-capabilities` with alias → real web mode +- **Agent sessions:** separate DeepSeek session per `user` / agent id +- **Session recovery:** auto-reset of stale chains/sessions +- **Zero dependencies:** Node.js 18+, no npm dependencies +- **Chrome Extension:** Browser extension for session management +- **Dashboard Web:** Administrative web interface with real-time metrics --- -## ⚡ Быстрый старт +## ⚡ Quick Start ```bash git clone https://github.com/ForgetMeAI/FreeDeepseekAPI.git @@ -93,29 +97,29 @@ npm run auth npm start ``` -`npm run auth` открывает меню авторизации: +`npm run auth` opens the auth menu: -1. выберите пункт `1`; -2. войдите в DeepSeek в отдельном Chrome-профиле; -3. отправьте короткое сообщение вроде `ok`; -4. вернитесь в терминал и нажмите Enter. +1. select item `1`; +2. log in to DeepSeek in a separate Chrome profile; +3. send a short message like `ok`; +4. return to terminal and press Enter. -`npm start` показывает меню запуска: +`npm start` shows the launch menu: -- `1` — авторизоваться / обновить DeepSeek login -- `2` — показать модели и статусы -- `3` — запустить proxy -- `4` — выйти +- `1` — authorize / update DeepSeek login +- `2` — show models and statuses +- `3` — start proxy +- `4` — exit -Для headless/CI-запуска без меню: +For headless/CI launch without menu: ```bash NON_INTERACTIVE=1 npm start -# или +# or SKIP_ACCOUNT_MENU=1 npm start ``` -По умолчанию сервер слушает: +By default the server listens on: ```text http://localhost:9655 @@ -123,7 +127,7 @@ http://localhost:9655 --- -## 🪟 Windows запуск +## Windows Launch ```powershell git clone https://github.com/ForgetMeAI/FreeDeepseekAPI.git @@ -132,18 +136,18 @@ npm run auth npm start ``` -Если Chrome установлен нестандартно, явно укажите путь: +If Chrome is installed in a non-standard location, specify the path explicitly: ```powershell $env:CHROME_PATH="C:\Program Files\Google\Chrome\Application\chrome.exe" npm run auth ``` -Если Chrome не найден, `npm run auth` теперь печатает готовые инструкции для Windows/macOS/Linux вместо загадочного stack trace. +If Chrome is not found, `npm run auth` now prints ready-to-use instructions for Windows/macOS/Linux instead of a cryptic stack trace. --- -## 🐧 Linux / Chromium запуск +## Linux / Chromium Launch ```bash git clone https://github.com/ForgetMeAI/FreeDeepseekAPI.git @@ -152,33 +156,33 @@ CHROME_PATH=$(which chromium) npm run auth npm start ``` -Если Chromium называется иначе: +If Chromium has a different name: ```bash CHROME_PATH=$(which chromium-browser) npm run auth -# или +# or CHROME_PATH=$(which google-chrome) npm run auth ``` --- -## 🖥 VPS / headless запуск +## VPS / Headless Launch -Самый надёжный flow без Chrome на сервере: +The most reliable flow without Chrome on the server: -1. На домашнем ПК, где есть GUI/Chrome: +1. On your home PC (where you have GUI/Chrome): ```bash npm run auth ``` -2. Скопируйте `deepseek-auth.json` на VPS: +2. Copy `deepseek-auth.json` to VPS: ```bash scp deepseek-auth.json user@your-vps:/opt/FreeDeepseekAPI/deepseek-auth.json ``` -3. На VPS импортируйте/проверьте файл и выставьте безопасные права: +3. On VPS import/verify the file and set safe permissions: ```bash cd /opt/FreeDeepseekAPI @@ -186,87 +190,87 @@ npm run auth:import -- --input ./deepseek-auth.json npm run doctor -- --offline ``` -4. Запускайте proxy без интерактивного меню: +4. Start proxy without interactive menu: ```bash NON_INTERACTIVE=1 npm start ``` -Можно импортировать не только готовый `deepseek-auth.json`, но и browser cookie export: +You can import not only a ready-made `deepseek-auth.json`, but also a browser cookie export: ```bash DEEPSEEK_TOKEN="" npm run auth:import -- --input ./cookies.json ``` -> Важно: `deepseek-auth.json` — это доступ к вашему DeepSeek Web login. Не коммитьте, не публикуйте, храните с правами `0600`. +> Important: `deepseek-auth.json` gives access to your DeepSeek Web login. Do not commit, do not publish, store with `0600` permissions. --- -## 🩺 Diagnostics / doctor +## Diagnostics / doctor ```bash npm run doctor -# без сетевых запросов к DeepSeek: +# without network requests to DeepSeek: npm run doctor -- --offline ``` -`doctor` проверяет: +`doctor` checks: -- найден ли `deepseek-auth.json` / `DEEPSEEK_AUTH_DIR`; -- валидный ли JSON; -- есть ли `token`, `cookie`, `wasmUrl`; -- безопасные ли права файла на macOS/Linux (`0600`); -- при обычном запуске — доступен ли DeepSeek PoW endpoint. +- whether `deepseek-auth.json` / `DEEPSEEK_AUTH_DIR` is found; +- whether JSON is valid; +- whether `token`, `cookie`, `wasmUrl` exist; +- whether file permissions are safe on macOS/Linux (`0600`); +- on normal run — whether DeepSeek PoW endpoint is reachable. -Если видите `data.biz_data is null`, `fetch failed`, `401/403/429` или Hermes/OpenCode не видит модели — первым делом запускайте `npm run doctor`. +If you see `data.biz_data is null`, `fetch failed`, `401/403/429` or Hermes/OpenCode doesn't see models — first run `npm run doctor`. --- -## ♻️ Session reuse и сброс чатов +## ♻️ Session reuse and chat reset -FreeDeepseekAPI не создаёт новый DeepSeek чат на каждый HTTP-запрос без причины. Логика такая: +FreeDeepseekAPI doesn't create a new DeepSeek chat on every HTTP request without reason. The logic is: -- один `x-agent-session`, `session` или `user` → одна DeepSeek chat session; -- если session id уже есть — proxy переиспользует его и продолжает chain через `parent_message_id`; -- auto-reset происходит при TTL, ошибке DeepSeek session или слишком длинной цепочке сообщений; -- локальная history сохраняется коротким контекстом, чтобы новая DeepSeek session могла продолжить разговор. +- one `x-agent-session`, `session`, or `user` → one DeepSeek chat session; +- if session id already exists — proxy reuses it and continues chain via `parent_message_id`; +- auto-reset happens on TTL, DeepSeek session error, or too long message chain; +- local history is saved as short context so new DeepSeek session can continue the conversation. -Явно задать agent/session: +Explicitly set agent/session: ```bash curl -X POST http://localhost:9655/v1/chat/completions \ -H "Content-Type: application/json" \ -H "x-agent-session: my-agent" \ - -d '{"model":"deepseek-chat","messages":[{"role":"user","content":"Привет"}]}' + -d '{"model":"deepseek-chat","messages":[{"role":"user","content":"Hi"}]}' ``` -Посмотреть активные sessions: +View active sessions: ```bash curl http://localhost:9655/v1/sessions ``` -Сбросить одну session: +Reset a single session: ```bash curl -X POST "http://localhost:9655/reset-session?agent=my-agent" ``` -Сбросить все sessions: +Reset all sessions: ```bash curl -X POST "http://localhost:9655/reset-session?agent=all" ``` -Почему чаты всё равно появляются в DeepSeek Web: proxy работает через внутренний Web Chat API, а DeepSeek хранит реальные chat sessions у себя. Это нормально для web-proxy. Задача session reuse — не плодить новые чаты без необходимости и аккуратно сбрасываться только когда chain протух/сломался. +Why chats still appear in DeepSeek Web: proxy works through internal Web Chat API, and DeepSeek stores real chat sessions on their side. This is normal for web-proxy. The task of session reuse is not to spawn new chats unnecessarily and to reset cleanly only when the chain has gone stale/broken. --- -## 👥 Multi-account pool +## Multi-account pool -Можно подключить несколько auth-файлов. Правильная модель: sticky account per agent/session — proxy не переключает аккаунт внутри живой DeepSeek-сессии. Если аккаунт получил `401/403/429` и ушёл в cooldown, session безопасно сбрасывается и новый запрос может перейти на другой доступный аккаунт. +You can connect multiple auth files. Correct model: sticky account per agent/session — proxy doesn't switch account inside a live DeepSeek session. If an account gets `401/403/429` and goes into cooldown, the session is safely reset and a new request may switch to another available account. -Вариант 1 — директория с auth-файлами: +Option 1 — directory with auth files: ```bash mkdir -p accounts @@ -276,22 +280,22 @@ chmod 600 accounts/*.json DEEPSEEK_AUTH_DIR=./accounts NON_INTERACTIVE=1 npm start ``` -Вариант 2 — список файлов: +Option 2 — file list: ```bash DEEPSEEK_AUTH_PATH="./accounts/main.json,./accounts/backup.json" NON_INTERACTIVE=1 npm start ``` -Как работает pool: +How the pool works: -- новый agent/session получает доступный аккаунт round-robin; -- выбранный аккаунт закрепляется за session (`sticky`); -- при `401`, `403`, `429` аккаунт уходит в cooldown; -- если sticky-аккаунт session ушёл в cooldown, старая DeepSeek-сессия сбрасывается, чтобы не долбить rate-limited/expired аккаунт; -- статус аккаунтов виден в `/health` без путей к auth-файлам и без имён файлов; -- auth-файлы должны храниться с правами `0600`. +- new agent/session gets an available account round-robin; +- selected account is pinned to session (`sticky`); +- on `401`, `403`, `429` account goes into cooldown; +- if sticky-account session went into cooldown, old DeepSeek session is reset to avoid hammering rate-limited/expired account; +- account status visible in `/health` without auth file paths or file names; +- auth files must be stored with `0600` permissions. -Настроить cooldown: +Configure cooldown: ```bash DEEPSEEK_ACCOUNT_COOLDOWN_MS=600000 npm start @@ -299,22 +303,22 @@ DEEPSEEK_ACCOUNT_COOLDOWN_MS=600000 npm start --- -## 🔑 Идеи для консольной авторизации +## Console auth ideas -Парольный flow из PR #3 можно делать, но безопаснее не хранить пароль и не делать это дефолтом. Нормальная реализация: +Password flow from PR #3 can be done, but safer not to store password and not make it default. Normal implementation: -1. `npm run auth:console` спрашивает email/телефон и пароль через hidden prompt. -2. Пароль держится только в памяти процесса, не пишется в файлы/logs/history. -3. Скрипт повторяет Web login flow через `fetch`/CDP: получает captcha/verify challenge, отдаёт человеку ссылку/код, ждёт подтверждение. -4. После успешного login сохраняется только `deepseek-auth.json` стандартного формата. -5. Если DeepSeek просит captcha/2FA — скрипт честно говорит “открой ссылку, пройди проверку, нажми Enter”, а не пытается обходить защиту. -6. Для VPS лучше режим `auth:console --no-save-password --output deepseek-auth.json`. +1. `npm run auth:console` asks for email/phone and password via hidden prompt. +2. Password stays only in process memory, not written to files/logs/history. +3. Script replicates Web login flow via `fetch`/CDP: gets captcha/verify challenge, gives human link/code, waits for confirmation. +4. After successful login, only standard-format `deepseek-auth.json` is saved. +5. If DeepSeek asks for captcha/2FA — script honestly says "open link, pass check, press Enter", doesn't try to bypass protection. +6. For VPS better mode `auth:console --no-save-password --output deepseek-auth.json`. -Минимальный безопасный MVP: console auth только интерактивный, без env-пароля. Допустимый automation-вариант: `DEEPSEEK_EMAIL=... npm run auth:console`, но пароль всё равно вводится hidden prompt. +Minimal safe MVP: console auth only interactive, no env password. Acceptable automation variant: `DEEPSEEK_EMAIL=... npm run auth:console`, but password still entered via hidden prompt. --- -## ✅ Проверка работы +## ✅ Verify it works ```bash curl http://localhost:9655/ @@ -322,11 +326,11 @@ curl http://localhost:9655/v1/models curl http://localhost:9655/v1/model-capabilities ``` -Если всё ок, `/health` вернёт статус сервера, список поддерживаемых aliases и `config_ready: true`. +If all good, `/health` returns server status, list of supported aliases, and `config_ready: true`. --- -## 🧪 Примеры запросов +## Request examples ### Chat Completions @@ -335,7 +339,7 @@ curl -X POST http://localhost:9655/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-chat", - "messages": [{"role": "user", "content": "Привет! Ответь одной фразой."}], + "messages": [{"role": "user", "content": "Hi! Reply with one phrase."}], "stream": false }' ``` @@ -347,18 +351,18 @@ curl -X POST http://localhost:9655/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-reasoner", - "messages": [{"role": "user", "content": "Реши коротко: почему небо голубое?"}], + "messages": [{"role": "user", "content": "Solve briefly: why is the sky blue?"}], "stream": false }' ``` -Для reasoning-моделей API отдаёт цепочку размышления отдельно от финального ответа: +For reasoning models, API returns the reasoning chain separately from the final answer: - non-stream: `choices[0].message.reasoning_content` - stream: `choices[0].delta.reasoning_content` - usage: `usage.completion_tokens_details.reasoning_tokens` -`reasoning_tokens` — приблизительная оценка по извлечённому DeepSeek Web `THINK`-тексту, потому что web stream не отдаёт официальный token usage по reasoning отдельно. +`reasoning_tokens` — approximate estimate based on extracted DeepSeek Web `THINK` text, because web stream doesn't return official token usage for reasoning separately. ### Web search @@ -367,7 +371,7 @@ curl -X POST http://localhost:9655/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-chat-search", - "messages": [{"role": "user", "content": "Найди свежий факт про DeepSeek и ответь кратко."}], + "messages": [{"role": "user", "content": "Find a fresh fact about DeepSeek and reply briefly."}], "stream": false }' ``` @@ -379,7 +383,7 @@ curl -N -X POST http://localhost:9655/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-chat", - "messages": [{"role": "user", "content": "Напиши короткую шутку."}], + "messages": [{"role": "user", "content": "Write a short joke."}], "stream": true }' ``` @@ -392,12 +396,12 @@ curl -X POST http://localhost:9655/v1/messages \ -d '{ "model": "deepseek-chat", "max_tokens": 512, - "messages": [{"role": "user", "content": "Ответь ровно OK"}], + "messages": [{"role": "user", "content": "Reply exactly OK"}], "stream": false }' ``` -Для Claude Code можно указывать backend напрямую: +For Claude Code you can specify backend directly: ```bash export ANTHROPIC_BASE_URL="http://127.0.0.1:9655" @@ -413,147 +417,145 @@ curl -X POST http://localhost:9655/v1/responses \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-chat", - "input": "Ответь ровно OK", + "input": "Reply exactly OK", "stream": false }' ``` ### Tool calling -FreeDeepseekAPI принимает: +FreeDeepseekAPI accepts: - OpenAI `tools`; - Anthropic `tools`; - Responses API function tools. -Прокси просит DeepSeek вернуть строгий JSON tool call, но также умеет парсить fallback-форматы: +Proxy asks DeepSeek to return strict JSON tool call, but also parses fallback formats: - `TOOL_CALL:` - fenced JSON -- `...` +- `...` --- -## 🧠 Модели +## Models -`GET /v1/models` возвращает только aliases, которые сейчас проверены и работают через этот proxy. +DeepSeek Web supports several model aliases. Use these in the `model` field: -### Рабочие aliases +- `deepseek-chat` — standard chat model +- `deepseek-reasoner` — reasoning model with chain-of-thought +- `deepseek-chat-search` — chat with web search enabled -| Alias | Web mode | Reasoning | Web search | Комментарий | -| --- | --- | --- | --- | --- | -| `deepseek-chat` | `Быстрый` / `default` | нет | нет | базовый chat | -| `deepseek-v3` | `Быстрый` / `default` | нет | нет | совместимый alias | -| `deepseek-default` | `Быстрый` / `default` | нет | нет | совместимый alias | -| `deepseek-reasoner` | `Быстрый` / `default` | да | нет | `thinking_enabled=true` | -| `deepseek-r1` | `Быстрый` / `default` | да | нет | R1-compatible alias | -| `deepseek-chat-search` | `Быстрый` / `default` | нет | да | web search | -| `deepseek-default-search` | `Быстрый` / `default` | нет | да | web search alias | -| `deepseek-reasoner-search` | `Быстрый` / `default` | да | да | reasoning + search | -| `deepseek-r1-search` | `Быстрый` / `default` | да | да | R1-compatible + search | -| `deepseek-expert` | `Эксперт` / `expert` | нет | нет | Expert mode | -| `deepseek-v4-pro` | `Эксперт` / `expert` | да | нет | Expert + reasoning | - -Полный маппинг: +For a full list, run: ```bash -curl http://localhost:9655/v1/model-capabilities +curl http://localhost:9655/v1/models ``` -По официальной странице DeepSeek V4 Preview `deepseek-chat` и `deepseek-reasoner` сейчас route'ятся в `deepseek-v4-flash` non-thinking/thinking. В самом `chat.deepseek.com` direct stream точное имя чекпойнта не отдаётся (`model: ""`), поэтому proxy фиксирует одновременно web-режим (`default` / `Быстрый`) и актуальную официальную маршрутизацию (`DeepSeek-V4-Flash`). - -Текущий вывод DeepSeek Web remote config показывает такие web-режимы: +--- -- `default` / UI `Быстрый` — работает; поддерживает `thinking_enabled` и `search_enabled`. -- `expert` / UI `Эксперт` — работает через актуальный web-контракт (`x-client-version=2.0.0`) и поддерживает `thinking_enabled`. В `/v1/models` выдаются `deepseek-expert` без reasoning и `deepseek-v4-pro` как Expert + reasoning. -- `vision` / UI `Распознавание` — виден в remote config, но сейчас direct Web API возвращает `backend_err_by_model` (`Vision is temporarily unavailable`). Поэтому `deepseek-vision` скрыт из `/v1/models`. +## Endpoints -Search для Expert по remote config недоступен, поэтому `deepseek-expert-search` остаётся unsupported. +- `GET /` — server status +- `GET /health` — detailed health check +- `GET /v1/models` — list available models +- `GET /v1/model-capabilities` — model capabilities +- `POST /v1/chat/completions` — OpenAI-compatible chat completions +- `POST /v1/messages` — Anthropic-compatible messages API +- `POST /v1/responses` — OpenAI Responses API +- `GET /v1/sessions` — list active sessions +- `POST /reset-session` — reset a session (query param `agent`) --- -## 🔌 Endpoints +## Open WebUI + +To connect Open WebUI to FreeDeepseekAPI: -| Method | Path | Назначение | -| --- | --- | --- | -| `GET` | `/` или `/health` | статус proxy | -| `GET` | `/v1/models` | список рабочих OpenAI-compatible aliases | -| `GET` | `/v1/model-capabilities` | полный маппинг aliases, real model, capabilities | -| `POST` | `/v1/chat/completions` | OpenAI-compatible Chat Completions | -| `POST` | `/v1/messages` | Anthropic Messages API shim | -| `POST` | `/v1/responses` | OpenAI Responses API shim | -| `GET` | `/v1/sessions` | активные локальные agent sessions | -| `POST` | `/reset-session?agent=` | сбросить одну session | -| `POST` | `/reset-session?agent=all` | сбросить все sessions | +1. Start FreeDeepseekAPI: `npm start` +2. In Open WebUI, go to Settings → Connections +3. Set API URL to `http://localhost:9655` +4. Use any dummy API key (e.g., `sk-dummy`) +5. Select `deepseek-chat` or other models --- -## 🖥 Open WebUI +## Chrome Extension -Base URL для Open WebUI в Docker: +A browser extension is available for session management. -```text -http://host.docker.internal:9655/v1 -``` +### Installation -Для локального запуска без Docker: +1. Open `chrome://extensions/` +2. Enable "Developer mode" +3. Click "Load unpacked" +4. Select the `chrome-extension/` folder -```text -http://localhost:9655/v1 -``` +### Features + +- Real-time server status +- Session refresh and auto-capture +- Popup interface for quick actions +- Cookie-based session monitoring +- Request interception for DeepSeek API -API key можно указать любой: proxy сам ходит в DeepSeek Web через сохранённую browser-сессию. +### Usage + +Click the extension icon to view: + +- Server connection status (`http://localhost:9655`) +- Active session ID +- Number of configured accounts +- Last activity timestamp --- -## 🔐 Обновить логин +## Dashboard Web + +A web dashboard provides advanced administration. + +### Access -```bash -npm run auth -npm start +``` +http://localhost:9655/dashboard ``` -Если DeepSeek начал отвечать `401`, `403` или просит новый PoW/session — повторите `npm run auth` и обновите сохранённую browser-сессию. +### Features -Локальные файлы авторизации не должны попадать в GitHub: +- Active session monitoring +- API usage statistics +- Account management +- Real-time metrics -- `deepseek-auth.json` -- `.chrome-profile-deepseek/` -- `.env` +### Development -Они уже добавлены в `.gitignore`. +```bash +cd dashboard +npm install +npm start # Development server +npm run build # Production build +``` --- -## 🧪 Тесты +## Update login -Синтаксическая проверка проекта: +If your DeepSeek session expires, refresh it: ```bash -npm test +npm run auth ``` -Live smoke-тесты против запущенного локального proxy: +Or use the import command with fresh credentials: ```bash -BASE_URL=http://127.0.0.1:9655 MODEL=deepseek-chat npm run test:live +npm run auth:import -- --input ./new-auth.json ``` --- -## 📌 Статус проекта - -FreeDeepseekAPI — экспериментальный web-chat proxy для локального использования и интеграций. Он зависит от текущего контракта DeepSeek Web Chat, поэтому при изменениях на стороне DeepSeek может потребоваться обновление auth/session logic или model mapping. +## Project status -Если что-то перестало работать: +This project is actively maintained. For updates and support, join the Telegram channel: [ForgetMeAI](https://t.me/forgetmeai). -1. обновите логин через `npm run auth`; -2. проверьте `/v1/model-capabilities`; -3. повторите запрос на свежей сессии; -4. если проблема сохраняется — вероятно, DeepSeek изменил внутренний Web API. - ---- - -

- ForgetMeAI · Telegram -

+Contributions are welcome via pull requests. diff --git a/chrome-extension/README.md b/chrome-extension/README.md index 169369d..425a81a 100644 --- a/chrome-extension/README.md +++ b/chrome-extension/README.md @@ -1,31 +1,33 @@ -# DeepSeek → FreeDeepseekAPI (расширение) +# DeepSeek → FreeDeepseekAPI (extension) -Добавляет аккаунт DeepSeek в локальный FreeDeepseekAPI **одним кликом**: -перехватывает заголовки реального запроса к `chat.deepseek.com/api/...` -(`token` из `Authorization`, все cookie, `hif_*`) и отправляет на +Adds a DeepSeek account to your local FreeDeepseekAPI **with one click**: +intercepts headers from a real request to `chat.deepseek.com/api/...` +(`token` from `Authorization`, all cookies, `hif_*`) and sends them to `http://localhost:9655/api/accounts/import`. +Works in Firefox and Chrome/Edge (Manifest V3). + Работает в Firefox и Chrome/Edge (Manifest V3). ## Установка **Firefox** -1. Откройте `about:debugging#/runtime/this-firefox` -2. «Загрузить временное дополнение» → выберите `manifest.json` из этой папки. - (Временное дополнение: после перезапуска Firefox установить заново.) +1. Open `about:debugging#/runtime/this-firefox` +2. "Load Temporary Add-on" → select `manifest.json` from this folder. + (Temporary add-on: after Firefox restart, install again.) **Chrome / Edge** -1. Откройте `chrome://extensions` -2. Включите «Режим разработчика». -3. «Загрузить распакованное» → выберите эту папку. +1. Open `chrome://extensions` +2. Enable "Developer mode". +3. "Load unpacked" → select this folder. -## Использование -1. Запустите FreeDeepseekAPI (порт 9655). -2. Откройте `chat.deepseek.com` и войдите в нужный аккаунт. -3. **Отправьте любое сообщение** (например `ok`) — чтобы прошёл запрос, из которого берутся креды. -4. Клик по иконке расширения → **«➕ Добавить в FreeDeepseekAPI»**. +## Usage +1. Start FreeDeepseekAPI (port 9655). +2. Open `chat.deepseek.com` and log in to the desired account. +3. **Send any message** (e.g. `ok`) — so the request from which creds are taken occurs. +4. Click extension icon → **"➕ Add to FreeDeepseekAPI"**. -Для нескольких аккаунтов повторите из разных профилей/логинов браузера. +For multiple accounts, repeat from different browser profiles/logins. -Вспомогательные кнопки: «Собрать» (показать креды), «Копировать JSON», -«Скачать файл» (`deepseek-auth.json`) — на случай ручного импорта через дашборд. +Auxiliary buttons: "Collect" (show creds), "Copy JSON", +"Download file" (`deepseek-auth.json`) — for manual import via dashboard. diff --git a/chrome-extension/background.js b/chrome-extension/background.js index 481c500..938fcbd 100644 --- a/chrome-extension/background.js +++ b/chrome-extension/background.js @@ -1,18 +1,18 @@ -// DeepSeek → FreeDeepseekAPI — перехват заголовков реального запроса. -// token (Authorization: Bearer), cookie (все), hif (x-hif-*) берутся из -// настоящего запроса к chat.deepseek.com/api/... — как в HAR/cURL. +// DeepSeek → FreeDeepseekAPI — intercepts headers of real request. +// token (Authorization: Bearer), cookie (all), hif (x-hif-*) are taken from +// actual request to chat.deepseek.com/api/... — same as HAR/cURL. const WASM_DEFAULT = 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm'; const KEY = 'deepseek_capture'; -// extraHeaders нужен Chrome для доступа к Cookie/Authorization; Firefox даёт их без него. +// extraHeaders needed for Chrome to access Cookie/Authorization; Firefox provides them without it. const opts = ['requestHeaders']; try { if (chrome.webRequest.OnBeforeSendHeadersOptions && chrome.webRequest.OnBeforeSendHeadersOptions.EXTRA_HEADERS) { opts.push('extraHeaders'); } -} catch (e) { /* Firefox: опции нет — это нормально */ } +} catch (e) { /* Firefox: option doesn't exist — this is normal */ } chrome.webRequest.onBeforeSendHeaders.addListener( (details) => { diff --git a/chrome-extension/content.js b/chrome-extension/content.js index ad04f8c..8e62399 100644 --- a/chrome-extension/content.js +++ b/chrome-extension/content.js @@ -13,4 +13,4 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } sendResponse({ data }); } -}); +}); \ No newline at end of file diff --git a/chrome-extension/manifest.json b/chrome-extension/manifest.json index 5501740..2030591 100644 --- a/chrome-extension/manifest.json +++ b/chrome-extension/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "DeepSeek → FreeDeepseekAPI", "version": "1.4", - "description": "Добавляет аккаунт DeepSeek в локальный FreeDeepseekAPI одним кликом. Перехватывает заголовки запроса chat.deepseek.com (token + cookie + hif).", + "description": "Adds a DeepSeek account to local FreeDeepseekAPI with one click. Intercepts chat.deepseek.com request headers (token + cookie + hif).", "author": "FreeDeepseekAPI", "browser_specific_settings": { "gecko": { @@ -27,4 +27,4 @@ "default_icon": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" } }, "icons": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" } -} +} \ No newline at end of file diff --git a/chrome-extension/popup.html b/chrome-extension/popup.html index 05f1be5..db990c1 100644 --- a/chrome-extension/popup.html +++ b/chrome-extension/popup.html @@ -57,19 +57,19 @@

🔑 DeepSeek → FreeDeepseekAPI

- +
-
Откройте chat.deepseek.com, отправьте любое сообщение, затем нажмите кнопку
+
Open chat.deepseek.com, send any message, then click the button
⏳ Loading...
- +
- - - + + +
@@ -81,10 +81,10 @@

🔑 DeepSeek → FreeDeepseekAPI

- Пул аккаунтов + Account Pool - - + +
@@ -92,4 +92,4 @@

🔑 DeepSeek → FreeDeepseekAPI

- + \ No newline at end of file diff --git a/chrome-extension/popup.js b/chrome-extension/popup.js index 423ea09..8bb0cbd 100644 --- a/chrome-extension/popup.js +++ b/chrome-extension/popup.js @@ -3,29 +3,29 @@ function $(id) { return document.getElementById(id); } const API_BASE = 'http://localhost:9655'; const PROXY_URL = API_BASE + '/api/accounts/import'; -let current = null; // перехваченный набор кредов {token,cookie,hif_*,wasmUrl} +let current = null; // intercepted creds set {token,cookie,hif_*,wasmUrl} function setStatus(cls, text) { $('status').className = 'status ' + cls; $('status').textContent = text; } -function setSrvDot(online) { const d = $('srvDot'); if (d) { d.className = 'srv-dot ' + (online ? 'online' : 'offline'); d.title = online ? 'Сервер онлайн (:9655)' : 'Сервер недоступен (:9655)'; } } +function setSrvDot(online) { const d = $('srvDot'); if (d) { d.className = 'srv-dot ' + (online ? 'online' : 'offline'); d.title = online ? 'Server online (:9655)' : 'Server unavailable (:9655)'; } } function setCredButtons(has) { for (const id of ['btnAdd', 'btnCopy', 'btnSave']) { const b = $(id); if (b) b.disabled = !has; } } function render(cap) { if (!cap || !cap.token || !cap.cookie) { - setStatus('warn', '⚠️ Откройте chat.deepseek.com и ОТПРАВЬТЕ любое сообщение, затем нажмите кнопку.'); + setStatus('warn', '⚠️ Open chat.deepseek.com and SEND any message, then click the button.'); $('jsonPreview').textContent = '{ }'; - $('detail').textContent = 'Креды появятся после запроса к DeepSeek'; + $('detail').textContent = 'Creds will appear after request to DeepSeek'; setCredButtons(false); return null; } const auth = { token: cap.token, hif_dliq: cap.hif_dliq || '', hif_leim: cap.hif_leim || '', cookie: cap.cookie, wasmUrl: cap.wasmUrl }; - // превью с маскировкой секретов + // preview with masked secrets $('jsonPreview').textContent = JSON.stringify({ token: auth.token.slice(0, 6) + '…(' + auth.token.length + ')', cookie: auth.cookie.slice(0, 48) + '…', hif_leim: auth.hif_leim ? ('…(' + auth.hif_leim.length + ')') : '', }, null, 2); - setStatus('ok', '✅ Перехвачено: token + cookie' + (auth.hif_leim ? ' + hif' : '') + ' — готово'); - $('detail').textContent = cap._t ? ('Обновлено: ' + new Date(cap._t).toLocaleTimeString()) : ''; + setStatus('ok', '✅ Captured: token + cookie' + (auth.hif_leim ? ' + hif' : '') + ' — ready'); + $('detail').textContent = cap._t ? ('Updated: ' + new Date(cap._t).toLocaleTimeString()) : ''; setCredButtons(true); return auth; } @@ -34,7 +34,7 @@ function refresh() { chrome.runtime.sendMessage({ action: 'get' }, (r) => { current = (r && r.success) ? render(r.cap) : render(null); }); } -// ── Панель пула (строится через DOM API, без innerHTML, чтобы исключить XSS) ── +// ── Pool panel (built via DOM API, no innerHTML, to rule out XSS) ── function mkBtn(act, label, title, cls) { const b = document.createElement('button'); b.className = cls; b.dataset.act = act; b.title = title; b.textContent = label; @@ -48,8 +48,8 @@ function setEmpty(text) { } function renderPool(list) { - $('poolTitle').textContent = `Пул аккаунтов (${list.length})`; - if (!list.length) { setEmpty('Нет аккаунтов'); return; } + $('poolTitle').textContent = `Account pool (${list.length})`; + if (!list.length) { setEmpty('No accounts'); return; } const pool = $('pool'); pool.textContent = ''; for (const a of list) { const row = document.createElement('div'); @@ -66,7 +66,7 @@ function renderPool(list) { const actions = document.createElement('span'); actions.className = 'row-actions'; - actions.append(mkBtn('check', '↻', 'Проверить', 'acc-btn'), mkBtn('del', '✕', 'Удалить', 'acc-btn danger')); + actions.append(mkBtn('check', '↻', 'Check', 'acc-btn'), mkBtn('del', '✕', 'Delete', 'acc-btn danger')); row.append(idEl, emailEl, badge, actions); pool.appendChild(row); @@ -81,8 +81,8 @@ async function loadPool() { renderPool(j.accounts || []); } catch { setSrvDot(false); - $('poolTitle').textContent = 'Пул аккаунтов'; - setEmpty('FreeDeepseekAPI недоступен на :9655'); + $('poolTitle').textContent = 'Account pool'; + setEmpty('FreeDeepseekAPI unavailable on :9655'); } } @@ -104,12 +104,12 @@ async function deleteAccount(id) { loadPool(); } -// делегирование кликов в панели (check / удаление с инлайн-подтверждением) +// click delegation in panel (check / delete with inline confirmation) $('pool').addEventListener('click', (e) => { const emailEl = e.target.closest('.email'); if (emailEl && emailEl.textContent && emailEl.textContent !== '—') { const full = emailEl.title || emailEl.textContent; - navigator.clipboard.writeText(full).then(() => { const o = emailEl.textContent; emailEl.textContent = '✓ скопировано'; setTimeout(() => { emailEl.textContent = o; }, 900); }); + navigator.clipboard.writeText(full).then(() => { const o = emailEl.textContent; emailEl.textContent = '✓ copied'; setTimeout(() => { emailEl.textContent = o; }, 900); }); return; } const btn = e.target.closest('.acc-btn'); if (!btn) return; @@ -118,7 +118,7 @@ $('pool').addEventListener('click', (e) => { if (act === 'check') { checkAccount(id); return; } if (act === 'del') { const actions = btn.parentElement; actions.textContent = ''; - actions.append(mkBtn('yes', '✓', 'Удалить', 'acc-btn confirm'), mkBtn('no', '✗', 'Отмена', 'acc-btn')); + actions.append(mkBtn('yes', '✓', 'Delete', 'acc-btn confirm'), mkBtn('no', '✗', 'Cancel', 'acc-btn')); return; } if (act === 'yes') deleteAccount(id); @@ -134,32 +134,32 @@ async function checkAll() { $('btnCheckAll').addEventListener('click', checkAll); $('btnDashboard').addEventListener('click', () => { chrome.tabs.create({ url: API_BASE + '/dashboard' }); }); -// ── Главная кнопка — добавить перехваченные креды + авто-валидация ── +// ── Main button — add captured creds + auto-validation ── $('btnAdd').addEventListener('click', async () => { if (!current) { refresh(); - setStatus('warn', '⏳ Кредов нет. Отправьте сообщение в DeepSeek и нажмите снова.'); + setStatus('warn', '⏳ No creds. Send a message in DeepSeek and try again.'); return; } - setStatus('warn', '⏳ Отправка в FreeDeepseekAPI…'); + setStatus('warn', '⏳ Sending to FreeDeepseekAPI…'); try { const r = await fetch(PROXY_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(current) }); const j = await r.json(); if (j.ok) { const who = j.email ? ` (${j.email})` : ''; - setStatus('ok', `✅ Добавлен как ${j.id}${who} — проверяю…`); + setStatus('ok', `✅ Added as ${j.id}${who} — checking…`); await loadPool(); const st = await checkAccount(j.id); - if (st === 'OK') setStatus('ok', `🟢 ${j.id}${who} — рабочий`); - else setStatus('err', `🔴 ${j.id} — статус: ${st || 'неизвестен'}`); + if (st === 'OK') setStatus('ok', `🟢 ${j.id}${who} — working`); + else setStatus('err', `🔴 ${j.id} — status: ${st || 'unknown'}`); } else if (j.existingId) { - setStatus('warn', `⚠️ Уже добавлен как ${j.existingId}`); + setStatus('warn', `⚠️ Already added as ${j.existingId}`); loadPool(); } else { - setStatus('err', '❌ ' + (j.error || 'Ошибка добавления')); + setStatus('err', '❌ ' + (j.error || 'Add error')); } } catch (e) { - setStatus('err', '❌ FreeDeepseekAPI недоступен на localhost:9655 (запущен?)'); + setStatus('err', '❌ FreeDeepseekAPI unavailable on localhost:9655 (running?)'); } }); @@ -168,7 +168,7 @@ $('btnCollect').addEventListener('click', refresh); $('btnCopy').addEventListener('click', () => { if (!current) return; navigator.clipboard.writeText(JSON.stringify(current, null, 2)).then(() => { - $('btnCopy').textContent = '✅'; setTimeout(() => { $('btnCopy').textContent = '📋 Копировать JSON'; }, 1200); + $('btnCopy').textContent = '✅'; setTimeout(() => { $('btnCopy').textContent = '📋 Copy JSON'; }, 1200); }); }); diff --git a/lib/parseAuth.js b/lib/parseAuth.js index b406925..df11c00 100644 --- a/lib/parseAuth.js +++ b/lib/parseAuth.js @@ -1,15 +1,15 @@ 'use strict'; /* - Общий парсер авторизации DeepSeek из "Copy as cURL" или HAR-файла. - Используется и CLI-скриптами (scripts/auth_from_*.js), и эндпоинтом - дашборда POST /api/accounts/import. Возвращает плоский объект - { token, cookie, hif_dliq, hif_leim, wasmUrl } или { error }. + Common DeepSeek auth parser from "Copy as cURL" or HAR file. + Used by both CLI scripts (scripts/auth_from_*.js) and dashboard + endpoint POST /api/accounts/import. Returns flat object + { token, cookie, hif_dliq, hif_leim, wasmUrl } or { error }. */ const WASM_DEFAULT = 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm'; // -H 'name: value' | -H "name: value" | -H $'name: value' (Chrome bash ANSI-C) | --header ... -// Опциональный \$? перед кавычкой покрывает форму "Copy as cURL (bash)". +// Optional \$? before the quote covers Chrome's "Copy as cURL (bash)" form. function extractHeadersFromCurl(curl) { const headers = {}; const re = /(?:-H|--header)\s+\$?(['"])(.+?):\s?([\s\S]*?)\1(?=\s|$)/g; @@ -32,7 +32,7 @@ function fromHeaders(h) { } function parseCurl(curl) { - // снять bash line-continuations (\ + перевод строки), которые Chrome вставляет в "Copy as cURL" + // strip bash line-continuations (\ + newline) that Chrome inserts in "Copy as cURL" const s = String(curl || '').replace(/\\\r?\n/g, ' '); const r = fromHeaders(extractHeadersFromCurl(s)); const wm = s.match(/https?:\/\/[^\s'"]*sha3[^\s'"]*\.wasm/i); @@ -43,11 +43,11 @@ function parseCurl(curl) { function parseHar(harText) { let har; try { har = (typeof harText === 'object') ? harText : JSON.parse(harText); } - catch { return { error: 'Не удалось прочитать HAR (не JSON)' }; } + catch { return { error: 'Failed to parse HAR (not valid JSON)' }; } const entries = (har.log && har.log.entries) || []; const hv = (hs, n) => { const x = (hs || []).find(y => (y.name || '').toLowerCase() === n); return x ? (x.value || '') : ''; }; - // выбираем лучший запрос к deepseek с Authorization: Bearer + // pick best request to deepseek with Authorization: Bearer let best = null; for (const e of entries) { const req = e.request || {}; @@ -63,40 +63,40 @@ function parseHar(harText) { best = { score, token: auth.replace(/^Bearer\s+/i, '').trim(), cookie, hif_dliq: dliq, hif_leim: leim }; } } - if (!best) return { error: 'В HAR нет запросов к deepseek.com с заголовком Authorization: Bearer' }; + if (!best) return { error: 'No requests to deepseek.com with Authorization: Bearer header in HAR' }; let wasmUrl = ''; for (const e of entries) { const u = (e.request && e.request.url) || ''; if (/sha3.*\.wasm/i.test(u)) { wasmUrl = u; break; } } return { token: best.token, cookie: best.cookie, hif_dliq: best.hif_dliq, hif_leim: best.hif_leim, wasmUrl }; } -// Авто-определение формата ввода (HAR — JSON с log.entries; иначе cURL). +// Auto-detect input format (HAR — JSON with log.entries; otherwise cURL). function parseAuthInput(text) { const s = String(text || '').trim(); - if (!s) return { error: 'Пустой ввод' }; + if (!s) return { error: 'Empty input' }; if (s[0] === '{' || s[0] === '[') { - // готовый JSON {token,cookie,...} (например, из расширения-экспортёра) + // ready-made JSON {token,cookie,...} (e.g. from a browser extension export) try { const o = JSON.parse(s); if (o && typeof o === 'object' && o.token && o.cookie) { return { token: String(o.token), cookie: String(o.cookie), hif_dliq: o.hif_dliq || '', hif_leim: o.hif_leim || '', wasmUrl: o.wasmUrl || '' }; } - } catch { /* не JSON-объект — пробуем HAR ниже */ } + } catch { /* not a plain JSON object — try HAR below */ } const r = parseHar(s); - if (!r.error) return r; // это был HAR + if (!r.error) return r; // it was a HAR } if (/\bcurl\b|--header|(^|\s)-H\s/i.test(s)) return parseCurl(s); - // последняя попытка — вдруг HAR без явного префикса + // last attempt — maybe HAR without explicit prefix return parseHar(s); } -// Валидация + проставление wasmUrl (из ввода → из прошлого аккаунта → дефолт). +// Validation + fill wasmUrl (from input → from previous account → default). function finalizeAuth(parsed, prevWasmUrl) { - if (!parsed || parsed.error) return parsed || { error: 'Пусто' }; + if (!parsed || parsed.error) return parsed || { error: 'Empty' }; const missing = []; if (!parsed.token) missing.push('token (authorization: Bearer)'); if (!parsed.cookie) missing.push('cookie'); - if (missing.length) return { error: 'Не найдено: ' + missing.join(', ') }; + if (missing.length) return { error: 'Missing: ' + missing.join(', ') }; return { token: parsed.token, hif_dliq: parsed.hif_dliq || '', diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1dcd0ad --- /dev/null +++ b/package-lock.json @@ -0,0 +1,20 @@ +{ + "name": "free-deepseek-api", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "free-deepseek-api", + "version": "0.1.0", + "license": "MIT", + "bin": { + "free-deepseek-api": "server.js", + "free-deepseek-client": "client.js" + }, + "engines": { + "node": ">=18.0.0" + } + } + } +} diff --git a/public/dashboard.html b/public/dashboard.html index 8ce1a9c..a0bece4 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -3,7 +3,7 @@ -FreeDeepseekAPI — Дашборд +FreeDeepseekAPI — Dashboard @@ -93,7 +93,7 @@ .chip{font-family:var(--mono);font-size:var(--fs-xs);background:var(--card2);border:1px solid var(--border);border-radius:6px;padding:7px 10px;cursor:pointer;transition:.12s;display:flex;align-items:center;gap:6px;color:var(--text)} .chip:hover{border-color:var(--accent)} - /* аккаунты — прямые полосы статуса */ + /* accounts — direct status bars */ .spacer{flex:1} .acc{display:flex;align-items:center;gap:var(--s3);padding:10px 12px;background:var(--card2);border:1px solid var(--border);border-left:3px solid var(--faint);border-radius:0 var(--r) var(--r) 0;margin-bottom:6px;flex-wrap:wrap} .acc.OK{border-left-color:var(--ok)} .acc.WAIT{border-left-color:var(--warn)} @@ -122,7 +122,7 @@ .bubble .role{font-size:10px;text-transform:uppercase;letter-spacing:.06em;opacity:.65;margin-bottom:4px;font-weight:700} .bubble.streaming .body::after{content:"▍";animation:blink 1s steps(2) infinite;color:var(--accent)} @keyframes blink{50%{opacity:0}} - /* панель размышлений */ + /* reasoning panel */ .think{margin-bottom:8px;border:1px solid var(--border);border-radius:8px;background:var(--bg2);overflow:hidden} .think summary{cursor:pointer;padding:6px 10px;color:var(--muted);font-weight:600;font-size:var(--fs-xs);display:flex;align-items:center;gap:6px;user-select:none;list-style:none} .think summary::-webkit-details-marker{display:none} @@ -179,85 +179,85 @@

FreeDeepseekAPI

- проверка… + checking…
-
@@ -268,17 +268,17 @@

Модели (`; function toast(m, kind){ const t=$('#toast'); t.innerHTML=(kind==='err'?icon('i-warn'):kind==='ok'?icon('i-check'):'')+''+esc(m)+''; t.className='toast show'+(kind?' '+kind:''); clearTimeout(toast._t); toast._t=setTimeout(()=>t.className='toast',1900); } async function copyText(t){ - try{ if(navigator.clipboard && isSecureContext){ await navigator.clipboard.writeText(t); return toast('Скопировано','ok'); } throw 0; } - catch{ try{ const a=document.createElement('textarea'); a.value=t; a.style.position='fixed'; a.style.opacity='0'; document.body.appendChild(a); a.select(); document.execCommand('copy'); a.remove(); toast('Скопировано','ok'); }catch{ toast('Не удалось скопировать','err'); } } + try{ if(navigator.clipboard && isSecureContext){ await navigator.clipboard.writeText(t); return toast('Copied','ok'); } throw 0; } + catch{ try{ const a=document.createElement('textarea'); a.value=t; a.style.position='fixed'; a.style.opacity='0'; document.body.appendChild(a); a.select(); document.execCommand('copy'); a.remove(); toast('Copied','ok'); }catch{ toast('Failed to copy','err'); } } } function mdRender(text){ try{ if(window.marked&&window.DOMPurify){ const html=window.marked.parse(text,{breaks:true}); return window.DOMPurify.sanitize(html,{ADD_ATTR:['target']}); } }catch{} return null; } -// стабильный agent-id для серверной сессии DeepSeek (многоходовость) +// stable agent-id for server-side DeepSeek session (multi-turn) const AGENT_ID = (()=>{ let v=localStorage.getItem('ds_agent'); if(!v){ v='dash-'+Math.abs(Date.now()^(performance.now()*1000|0)).toString(36); localStorage.setItem('ds_agent',v); } return v; })(); const apiBase = origin+'/v1'; $('#baseUrl').textContent = apiBase; -// ── Вкладки ────────────────────────────────────────────────────────────── +// ── Tabs ────────────────────────────────────────────────────────────── const TABS=['chat','accounts','overview']; const inited={}; function showTab(name){ @@ -304,10 +304,10 @@

Модели (copyText(`from openai import OpenAI\nclient = OpenAI(base_url="${apiBase}", api_key="sk-deepseek")\nr = client.chat.completions.create(model="deepseek-chat",\n messages=[{"role":"user","content":"Привет"}])\nprint(r.choices[0].message.content)`); +$('#copyCurl').onclick=()=>copyText(`curl ${apiBase}/chat/completions \\\n -H "Authorization: Bearer sk-deepseek" \\\n -H "Content-Type: application/json" \\\n -d '{"model":"deepseek-chat","messages":[{"role":"user","content":"Hi"}]}'`); +$('#copyPy').onclick=()=>copyText(`from openai import OpenAI\nclient = OpenAI(base_url="${apiBase}", api_key="sk-deepseek")\nr = client.chat.completions.create(model="deepseek-chat",\n messages=[{"role":"user","content":"Hi"}])\nprint(r.choices[0].message.content)`); -// ── Статус / KPI / авторизация ─────────────────────────────────────────── +// ── Status / KPI / Auth ─────────────────────────────────────────── let healthCache=null; async function loadHealth(){ try{ @@ -315,84 +315,84 @@

Модели (Авторизация не настроена. Запустите Авторизация DeepSeek.bat и войдите в свой аккаунт DeepSeek.

'; return; } - const exp=j.token_exp?('
'+esc(fmtExp(j.token_exp))+'
'):''; - box.innerHTML=`
${j.has_token?'есть ✓':'нет'}
-
${j.has_cookie?'есть ✓':'нет'}
-
${j.has_hif?'есть ✓':'опционально'}
${exp}`; - }catch{ box.innerHTML='
Не удалось получить статус авторизации
'; } + if(!j.config_ready){ box.innerHTML='
Authorization not configured. Run DeepSeek Authorization.bat and log into your DeepSeek account.
'; return; } + const exp=j.token_exp?('
'+esc(fmtExp(j.token_exp))+'
'):''; + box.innerHTML=`
${j.has_token?'present ✓':'missing'}
+
${j.has_cookie?'present ✓':'missing'}
+
${j.has_hif?'present ✓':'optional'}
${exp}`; + }catch{ box.innerHTML='
Failed to get authorization status
'; } } -// ── Аккаунты ─────────────────────────────────────────────────────────────── -const accStLabel=s=>({OK:'активен',WAIT:'лимит',INVALID:'невалиден',EXPIRED:'истёк',ERROR:'ошибка'})[s]||s; -function fmtResetAt(iso){ try{ return new Date(iso).toLocaleString('ru-RU',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}); }catch{ return ''; } } +// ── Accounts ─────────────────────────────────────────────────────────────── +const accStLabel=s=>({OK:'active',WAIT:'limit',INVALID:'invalid',EXPIRED:'expired',ERROR:'error'})[s]||s; +function fmtResetAt(iso){ try{ return new Date(iso).toLocaleString('en-US',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}); }catch{ return ''; } } async function loadAccountsList(){ const list=$('#accList'); try{ const j=await (await fetch(origin+'/api/accounts')).json(); - if(!j.accounts||!j.accounts.length){ list.innerHTML='
Пока нет аккаунтов. Добавьте первый через импорт ниже.
'; return; } + if(!j.accounts||!j.accounts.length){ list.innerHTML='
No accounts yet. Add first via import below.
'; return; } list.innerHTML=j.accounts.map(a=>{ - const meta=a.resetAt?('лимит до '+fmtResetAt(a.resetAt)):(a.exp?fmtExp(a.exp):''); + const meta=a.resetAt?('limit until '+fmtResetAt(a.resetAt)):(a.exp?fmtExp(a.exp):''); const email=a.email?`${esc(a.email)}`:''; const labelHtml=a.label?`${esc(a.label)}`:''; return `
${esc(a.id)}${labelHtml}${accStLabel(a.status)}${email}…${esc(a.preview||'')}${esc(meta)} - - -
`; + + +
`; }).join(''); setAccUpdated(); - }catch{ list.innerHTML='
Не удалось загрузить аккаунты.
'; } + }catch{ list.innerHTML='
Failed to load accounts.
'; } } -function setAccUpdated(){ const e=$('#accUpdated'); if(e) e.textContent='обновлено '+new Date().toLocaleTimeString('ru-RU',{hour:'2-digit',minute:'2-digit'}); } +function setAccUpdated(){ const e=$('#accUpdated'); if(e) e.textContent='updated '+new Date().toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit'}); } $('#checkAllAcc').onclick=async()=>{ const b=$('#checkAllAcc'); b.disabled=true; try{ const j=await (await fetch(origin+'/api/accounts')).json(); for(const a of (j.accounts||[])){ try{ await fetch(origin+'/api/accounts/'+encodeURIComponent(a.id)+'/check',{method:'POST'}); }catch{} } - await loadAccountsList(); await loadHealth(); toast('Проверка завершена','ok'); - }catch{ toast('Ошибка','err'); } b.disabled=false; + await loadAccountsList(); await loadHealth(); toast('Check complete','ok'); + }catch{ toast('Error','err'); } b.disabled=false; }; $('#importBtn').onclick=async()=>{ - const text=$('#importText').value.trim(); if(!text) return toast('Вставьте cURL или HAR','err'); + const text=$('#importText').value.trim(); if(!text) return toast('Paste cURL or HAR','err'); const b=$('#importBtn'); b.disabled=true; try{ const j=await (await fetch(origin+'/api/accounts/import',{method:'POST',headers:{'Content-Type':'text/plain'},body:text})).json(); - if(j.ok){ toast('Добавлен '+j.id,'ok'); $('#importText').value=''; loadAccountsList(); loadHealth(); } else toast(j.error||'Ошибка импорта','err'); - }catch{ toast('Сеть недоступна','err'); } b.disabled=false; + if(j.ok){ toast('Added '+j.id,'ok'); $('#importText').value=''; loadAccountsList(); loadHealth(); } else toast(j.error||'Import Error','err'); + }catch{ toast('Network unavailable','err'); } b.disabled=false; }; document.addEventListener('click', async e=>{ const t=e.target.closest('button'); if(!t) return; if(t.dataset.label!==undefined){ const acc=t.closest('.acc'); if(!acc) return; const cur=acc.querySelector('.acc-label')?.textContent||''; - const inp=document.createElement('input'); inp.type='text'; inp.value=cur; inp.placeholder='имя аккаунта'; inp.style.cssText='max-width:150px;padding:3px 7px'; + const inp=document.createElement('input'); inp.type='text'; inp.value=cur; inp.placeholder='account name'; inp.style.cssText='max-width:150px;padding:3px 7px'; acc.insertBefore(inp, acc.querySelector('.spacer')); inp.focus(); inp.select(); inp.addEventListener('keydown', async ev=>{ if(ev.key==='Enter'){ ev.preventDefault(); - try{ await fetch(origin+'/api/accounts/'+encodeURIComponent(t.dataset.label)+'/label',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({label:inp.value})}); toast('Имя сохранено','ok'); }catch{ toast('Ошибка','err'); } + try{ await fetch(origin+'/api/accounts/'+encodeURIComponent(t.dataset.label)+'/label',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({label:inp.value})}); toast('Name saved','ok'); }catch{ toast('Error','err'); } loadAccountsList(); } else if(ev.key==='Escape'){ loadAccountsList(); } }); - inp.addEventListener('blur', ()=>loadAccountsList()); // убрать input, если кликнули мимо + inp.addEventListener('blur', ()=>loadAccountsList()); // remove input if clicked away return; } - if(t.dataset.check){ const acc=t.closest('.acc'); const badge=acc&&acc.querySelector('.badge'); if(badge){ badge.className='badge checking'; badge.textContent='проверка'; } t.disabled=true; + if(t.dataset.check){ const acc=t.closest('.acc'); const badge=acc&&acc.querySelector('.badge'); if(badge){ badge.className='badge checking'; badge.textContent='checking'; } t.disabled=true; try{ await fetch(origin+'/api/accounts/'+encodeURIComponent(t.dataset.check)+'/check',{method:'POST'}); }catch{} await loadAccountsList(); await loadHealth(); return; } if(t.dataset.del){ - if(t.dataset.confirm!=='1'){ t.dataset.confirm='1'; t.classList.add('danger'); t.innerHTML=icon('i-trash')+'точно?'; setTimeout(()=>{ if(t.dataset.confirm==='1'){ t.dataset.confirm=''; t.classList.remove('danger'); t.innerHTML=icon('i-trash'); } },3000); return; } + if(t.dataset.confirm!=='1'){ t.dataset.confirm='1'; t.classList.add('danger'); t.innerHTML=icon('i-trash')+'sure?'; setTimeout(()=>{ if(t.dataset.confirm==='1'){ t.dataset.confirm=''; t.classList.remove('danger'); t.innerHTML=icon('i-trash'); } },3000); return; } t.disabled=true; - try{ await fetch(origin+'/api/accounts/'+encodeURIComponent(t.dataset.del),{method:'DELETE'}); toast('Удалён','ok'); }catch{ toast('Ошибка','err'); } loadAccountsList(); loadHealth(); return; } + try{ await fetch(origin+'/api/accounts/'+encodeURIComponent(t.dataset.del),{method:'DELETE'}); toast('Deleted','ok'); }catch{ toast('Error','err'); } loadAccountsList(); loadHealth(); return; } }); setInterval(()=>{ if($('#tab-accounts').classList.contains('active')) loadAccountsList(); }, 20000); -// ── Модели ───────────────────────────────────────────────────────────────── +// ── Models ───────────────────────────────────────────────────────────────── async function loadModels(){ const list=$('#modelList'); try{ @@ -401,12 +401,12 @@

Модели (R':'')+(c.search?'S':''); - return `${esc(m.id)}${caps}`; - }).join('')||'не удалось загрузить'; - }catch{ list.innerHTML='не удалось загрузить. '; } + const caps=(c.reasoning?'R':'')+(c.search?'S':''); + return `${esc(m.id)}${caps}`; + }).join('')||'failed to load'; + }catch{ list.innerHTML='failed to load. '; } } -// #chatModel наполняется реальными моделями из /v1/models; выбор сохраняется в localStorage +// #chatModel is populated with real models from /v1/models; selection saved to localStorage async function populateChatModels(){ const sel=$('#chatModel'); if(!sel) return; try{ @@ -415,21 +415,21 @@

Модели (${esc(m.id)}`).join(''); if(saved && data.some(m=>m.id===saved)) sel.value=saved; - }catch{ /* оставляем статичный fallback-список */ } + }catch{ /* keep static fallback list */ } } document.addEventListener('click', e=>{ const c=e.target.closest('.chip[data-model]'); if(c) copyText(c.dataset.model); }); -// Модель берётся напрямую из селектора (#chatModel). -// Веб-поиск (-search) сейчас отключён: DeepSeek Web стабильно отдаёт пустой ответ. +// Model is taken directly from selector (#chatModel). +// Web search (-search) currently disabled: DeepSeek Web consistently returns empty response. -// ── Чат (история + reasoning + markdown, через /v1/chat/completions) ────────── +// ── Chat (history + reasoning + markdown, via /v1/chat/completions) ────────── let chatHistory=[], chatBusy=false, chatAbort=null; function bubbleEl(m){ const d=document.createElement('div'); d.className='bubble '+m.role+(m.err?' err':''); - d.innerHTML='
'+(m.role==='user'?'Вы':'AI')+'
'; + d.innerHTML='
'+(m.role==='user'?'You':'AI')+'
'; if(m.role==='assistant' && m.reasoning){ const det=document.createElement('details'); det.className='think'; - det.innerHTML=''+icon('i-bulb')+'Размышления
'; + det.innerHTML=''+icon('i-bulb')+'Reasoning
'; det.querySelector('.think-body').textContent=m.reasoning; d.appendChild(det); d._think=det; } @@ -440,22 +440,22 @@

Модели ('+icon('i-chat')+'Начните диалог. Ответ — стримингом с markdown; у reasoner-моделей появится панель «Размышления».'; return; } + if(!chatHistory.length){ box.innerHTML='
'+icon('i-chat')+'Start dialog. Response streams with markdown; reasoner models will show \'Reasoning\' panel.
'; return; } box.innerHTML=''; chatHistory.forEach(m=>box.appendChild(bubbleEl(m))); box.scrollTop=box.scrollHeight; } function atBottom(){ const b=$('#msgs'); return b.scrollHeight-b.scrollTop-b.clientHeight<60; } -function setBusy(b){ chatBusy=b; const btn=$('#sendChat'); btn.innerHTML=b?'Стоп':icon('i-send')+'Отправить'; btn.classList.toggle('danger',b); } +function setBusy(b){ chatBusy=b; const btn=$('#sendChat'); btn.innerHTML=b?'Stop':icon('i-send')+'Send'; btn.classList.toggle('danger',b); } function ensureThink(node, asst){ if(node._think) return node._think.querySelector('.think-body'); const det=document.createElement('details'); det.className='think'; det.open=true; - det.innerHTML=''+icon('i-bulb')+'Размышления
'; + det.innerHTML=''+icon('i-bulb')+'Reasoning
'; node.insertBefore(det, node._c); node._think=det; return det.querySelector('.think-body'); } @@ -487,9 +487,9 @@

Модели (Модели (Модели ( { const args = new Set(process.argv.slice(2)); if (args.has('--help') || args.has('-h')) return printHelp(); - if (args.has('--login') || args.has('--add') || args.has('--relogin')) return void runDirectAuth(); + if (args.has('--login') || args.has('--add') || args.has('--relogin')) return void runChromeAuth(); if (args.has('--import')) return void runImportAuth(); if (args.has('--status') || args.has('--list')) return status(); if (args.has('--remove')) return removeLocalAuth(); diff --git a/scripts/auth_import.js b/scripts/auth_import.js index 96b8417..0d1c94e 100644 --- a/scripts/auth_import.js +++ b/scripts/auth_import.js @@ -74,18 +74,18 @@ Usage: DEEPSEEK_TOKEN="" npm run auth:import -- --input ./cookies.json Options: - --input, -i Source JSON: готовый deepseek-auth.json или browser cookie export + --input, -i Source JSON: ready deepseek-auth.json or browser cookie export --output, -o Target auth path (default: ${DEFAULT_OUT}) Security: - Для cookies.json передавайте token через DEEPSEEK_TOKEN, не через CLI argument, - чтобы не светить его в shell history/process list. + For cookies.json, pass the token via DEEPSEEK_TOKEN instead of a CLI argument + to avoid leaking it in shell history/process lists. VPS flow: - 1) На домашнем ПК: npm run auth - 2) Скопируй deepseek-auth.json на VPS - 3) На VPS: npm run auth:import -- --input ./deepseek-auth.json - 4) Запуск: NON_INTERACTIVE=1 npm start`); + 1) On your local PC: npm run auth + 2) Copy deepseek-auth.json to the VPS + 3) On VPS: npm run auth:import -- --input ./deepseek-auth.json + 4) Start: NON_INTERACTIVE=1 npm start`); } async function main(argv = process.argv.slice(2)) { const tokenArg = argValue(argv, '--token'); @@ -103,7 +103,7 @@ async function main(argv = process.argv.slice(2)) { const errors = validateAuth(auth); if (errors.length) { console.error(`[auth:import] Invalid auth import: ${errors.join(', ')}`); - console.error('[auth:import] Если импортируешь browser cookies, передай token через DEEPSEEK_TOKEN=...'); + console.error('[auth:import] When importing browser cookies, pass the token via DEEPSEEK_TOKEN=...'); return 2; } secureWriteJson(outputPath, auth); diff --git a/scripts/cdp-extract.js b/scripts/cdp-extract.js new file mode 100644 index 0000000..d8e2e6e --- /dev/null +++ b/scripts/cdp-extract.js @@ -0,0 +1,146 @@ +/* + Extract DeepSeek auth from a running Chrome instance via CDP. + + Usage (from auth.js): + const { extractAuth } = await import('./cdp-extract.js'); + const auth = await extractAuth('http://localhost:9222'); + + Returns { token, cookie, hif_dliq, hif_leim, wasmUrl } or null. +*/ + +class CDP { + constructor(wsUrl) { + this.ws = new WebSocket(wsUrl); + this.id = 0; + this.pending = new Map(); + this.events = []; + this.ws.onmessage = (ev) => { + const msg = JSON.parse(ev.data); + if (msg.id && this.pending.has(msg.id)) { + const { resolve, reject } = this.pending.get(msg.id); + this.pending.delete(msg.id); + msg.error + ? reject(new Error(JSON.stringify(msg.error))) + : resolve(msg.result); + } else if (msg.method) { + this.events.push(msg); + if (this.events.length > 1000) this.events.shift(); + } + }; + } + ready() { + return new Promise((resolve, reject) => { + this.ws.onopen = resolve; + this.ws.onerror = reject; + }); + } + send(method, params = {}) { + const id = ++this.id; + this.ws.send(JSON.stringify({ id, method, params })); + return new Promise((resolve, reject) => + this.pending.set(id, { resolve, reject }), + ); + } + close() { + try { this.ws.close(); } catch {} + } +} + +function normalizeToken(raw) { + if (!raw) return ''; + try { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object') + return parsed.value || parsed.token || parsed.access_token || parsed.accessToken || ''; + } catch {} + return String(raw).trim(); +} + +async function readPageAuth(cdp) { + const evalRes = await cdp.send('Runtime.evaluate', { + expression: `(() => { + const out = {href: location.href, localStorage:{}, sessionStorage:{}, resources: []}; + for (let i=0;i r.name).filter(n => /wasm|chat\\/completion|pow|chat_session/.test(n)).slice(-100); + return out; + })()`, + returnByValue: true, + }); + const pageState = evalRes.result.value || {}; + const stores = [pageState.localStorage || {}, pageState.sessionStorage || {}]; + + let token = ''; + for (const store of stores) { + for (const key of ['userToken', 'token', 'auth_token', 'access_token', 'accessToken']) { + token = normalizeToken(store[key]); + if (token) break; + } + if (token) break; + } + if (!token) { + for (const store of stores) { + for (const [k, v] of Object.entries(store)) { + if (/token/i.test(k)) { + token = normalizeToken(v); + if (token) break; + } + } + if (token) break; + } + } + + const cookieRes = await cdp.send('Network.getAllCookies'); + const cookies = (cookieRes.cookies || []).filter((c) => /deepseek\.com$/.test(c.domain)); + const cookie = cookies.map((c) => `${c.name}=${c.value}`).join('; '); + + let hif_dliq = '', hif_leim = ''; + for (const ev of cdp.events) { + const headers = ev.params?.headers || ev.params?.request?.headers; + if (!headers) continue; + for (const [k, v] of Object.entries(headers)) { + const lk = k.toLowerCase(); + if (lk === 'x-hif-dliq') hif_dliq = String(v); + if (lk === 'x-hif-leim') hif_leim = String(v); + if (lk === 'authorization' && !token && /^Bearer\s+/i.test(String(v))) + token = String(v).replace(/^Bearer\s+/i, ''); + } + } + + const wasmUrl = (pageState.resources || []).find((u) => /sha3.*\.wasm/.test(u)) || + 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm'; + + return { token, cookie, hif_dliq, hif_leim, wasmUrl }; +} + +async function getPageTarget(baseUrl) { + const resp = await fetch(`${baseUrl}/json`); + const targets = await resp.json(); + return targets.find((t) => /deepseek/i.test(t.url || '') && t.type === 'page') || targets.find((t) => t.type === 'page') || targets[0]; +} + +export async function extractAuth(cdpBaseUrl = 'http://localhost:9222') { + try { + const target = await getPageTarget(cdpBaseUrl); + if (!target?.webSocketDebuggerUrl) return null; + + const cdp = new CDP(target.webSocketDebuggerUrl); + await cdp.ready(); + await cdp.send('Runtime.enable'); + await cdp.send('Network.enable'); + + let auth = null; + for (let i = 0; i < 10; i++) { + auth = await readPageAuth(cdp); + if (auth.token && auth.cookie) break; + await new Promise((r) => setTimeout(r, 500)); + } + cdp.close(); + + if (!auth || !auth.token) return null; + const { href, cookiesCount, ...persisted } = auth; + return persisted; + } catch { + return null; + } +} diff --git a/scripts/deepseek_chrome_auth.js b/scripts/deepseek_chrome_auth.js index 91384fb..5423332 100755 --- a/scripts/deepseek_chrome_auth.js +++ b/scripts/deepseek_chrome_auth.js @@ -453,13 +453,13 @@ async function main() { await cdp.send('Network.enable'); console.log( - '\n[auth] Chrome открыт. Войди в DeepSeek в ЭТОМ отдельном окне.', + '\n[auth] Chrome opened. Log in to DeepSeek in THIS separate window.', ); console.log( - '[auth] После логина отправь в DeepSeek короткое сообщение, например: ok', + '[auth] After login, send a short message to DeepSeek, e.g.: ok', ); await ask( - '[auth] Когда залогинился и отправил тестовое сообщение — нажми ENTER здесь: ', + '[auth] Once logged in and sent a test message — press ENTER here: ', ); let auth = null; diff --git a/scripts/probe_deepseek_models.js b/scripts/probe_deepseek_models.js index c4ff2a0..c000b6f 100755 --- a/scripts/probe_deepseek_models.js +++ b/scripts/probe_deepseek_models.js @@ -77,7 +77,7 @@ async function probe(model_type, thinking_enabled, search_enabled) { chat_session_id: sessionId, parent_message_id: null, model_type, - prompt: 'Ответь ровно OK', + prompt: 'Reply with exactly OK', ref_file_ids: [], thinking_enabled, search_enabled, diff --git a/server.js b/server.js index 43dd96d..944bfe2 100755 --- a/server.js +++ b/server.js @@ -43,7 +43,7 @@ function printBanner() { ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███████ ███████ ██████ ███████ ███████ ███████ ██ ██ - FreeDeepseekAPI — API-прокси для DeepSeek Web Chat + FreeDeepseekAPI — API proxy for DeepSeek Web Chat ${formatWatermark()} `); } @@ -346,7 +346,9 @@ async function readDeepSeekJsonResponse(resp, label, account) { if (!resp.ok) markAccountFailure(account, resp.status, label); return { json, text }; } -loadDeepSeekConfig({ fatal: false }); +if (require.main === module) { + loadDeepSeekConfig({ fatal: false }); +} function createSession() { return { @@ -390,30 +392,30 @@ async function solvePOW(challenge, config = DS_CONFIG) { } const MODEL_CONFIGS = { - // DeepSeek Web real model_type: default / UI name: "Быстрый". + // DeepSeek Web real model_type: default / UI name: "Fast". // Public model family: DeepSeek-V3.2-Exp chat mode (fast, no visible reasoning). 'deepseek-chat': { model_type: 'default', thinking_enabled: false, search_enabled: false, - real_model: 'DeepSeek-V4-Flash non-thinking (DeepSeek Web “Быстрый” / default)', + real_model: 'DeepSeek-V4-Flash non-thinking (DeepSeek Web "Fast" / default)', capabilities: { reasoning: false, web_search: false, files: true }, supported: true, }, 'deepseek-v3': { model_type: 'default', thinking_enabled: false, search_enabled: false, - real_model: 'DeepSeek-V4-Flash non-thinking (DeepSeek Web “Быстрый” / default)', + real_model: 'DeepSeek-V4-Flash non-thinking (DeepSeek Web "Fast" / default)', capabilities: { reasoning: false, web_search: false, files: true }, supported: true, }, 'deepseek-default': { model_type: 'default', thinking_enabled: false, search_enabled: false, - real_model: 'DeepSeek-V4-Flash non-thinking (DeepSeek Web “Быстрый” / default)', + real_model: 'DeepSeek-V4-Flash non-thinking (DeepSeek Web "Fast" / default)', capabilities: { reasoning: false, web_search: false, files: true }, supported: true, }, // Same DeepSeek Web default model, but with thinking_enabled=true. UI exposes it as thinking/reasoning mode. 'deepseek-reasoner': { model_type: 'default', thinking_enabled: true, search_enabled: false, - real_model: 'DeepSeek-V4-Flash thinking mode (DeepSeek Web “Быстрый” + thinking_enabled)', + real_model: 'DeepSeek-V4-Flash thinking mode (DeepSeek Web "Fast" + thinking_enabled)', capabilities: { reasoning: true, web_search: false, files: true }, supported: true, }, @@ -425,13 +427,13 @@ const MODEL_CONFIGS = { }, 'deepseek-chat-search': { model_type: 'default', thinking_enabled: false, search_enabled: true, - real_model: 'DeepSeek-V4-Flash non-thinking (DeepSeek Web “Быстрый” / default) + web search', + real_model: 'DeepSeek-V4-Flash non-thinking (DeepSeek Web “Fast” / default) + web search', capabilities: { reasoning: false, web_search: true, files: true }, supported: true, }, 'deepseek-default-search': { model_type: 'default', thinking_enabled: false, search_enabled: true, - real_model: 'DeepSeek-V4-Flash non-thinking (DeepSeek Web “Быстрый” / default) + web search', + real_model: 'DeepSeek-V4-Flash non-thinking (DeepSeek Web “Fast” / default) + web search', capabilities: { reasoning: false, web_search: true, files: true }, supported: true, }, @@ -447,29 +449,29 @@ const MODEL_CONFIGS = { capabilities: { reasoning: true, web_search: true, files: true }, supported: true, }, - // DeepSeek Web UI name: “Эксперт”. Requires current web client headers (x-client-version=2.0.0). + // DeepSeek Web UI name: “Expert”. Requires current web client headers (x-client-version=2.0.0). 'deepseek-expert': { model_type: 'expert', thinking_enabled: false, search_enabled: false, - real_model: 'DeepSeek Web “Эксперт” (limited resources)', + real_model: 'DeepSeek Web “Expert” (limited resources)', capabilities: { reasoning: false, web_search: false, files: false }, supported: true, }, 'deepseek-v4-pro': { model_type: 'expert', thinking_enabled: true, search_enabled: false, - real_model: 'DeepSeek Web “Эксперт” + thinking mode (exposed as deepseek-v4-pro alias)', + real_model: 'DeepSeek Web “Expert” + thinking mode (exposed as deepseek-v4-pro alias)', capabilities: { reasoning: true, web_search: false, files: false }, supported: true, }, 'deepseek-expert-search': { model_type: 'expert', thinking_enabled: false, search_enabled: true, - real_model: 'DeepSeek Web “Эксперт” + search requested, but Expert has search_feature=null in remote config', + real_model: 'DeepSeek Web “Expert” + search requested, but Expert has search_feature=null in remote config', capabilities: { reasoning: false, web_search: false, files: false }, supported: false, unavailable_reason: 'Expert mode is rejected; remote config says search is not available for Expert.', }, 'deepseek-vision': { model_type: 'vision', thinking_enabled: false, search_enabled: false, - real_model: 'DeepSeek Web “Распознавание” / image understanding beta', + real_model: 'DeepSeek Web “Recognition” / image understanding beta', capabilities: { reasoning: false, web_search: false, files: true, vision: true }, supported: false, unavailable_reason: 'Current Web API returns: Vision is temporarily unavailable (backend_err_by_model).', @@ -488,6 +490,47 @@ const ALL_MODEL_CAPABILITIES = Object.fromEntries(Object.entries(MODEL_CONFIGS). unavailable_reason: cfg.unavailable_reason || null, }])); +function isAssistantOutputFragment(fragment) { + return fragment + && (fragment.type === 'RESPONSE' || fragment.type === 'SEARCH') + && typeof fragment.content === 'string'; +} + +function isReasoningFragment(fragment) { + return fragment + && (fragment.type === 'THINK' || fragment.type === 'REASONING') + && typeof fragment.content === 'string'; +} + +function isDeepSeekModelErrorEvent(event) { + return event && event.type === 'error'; +} + +function rebuildFragmentText(fragments) { + const responseText = fragments + .filter(isAssistantOutputFragment) + .map(f => f.content) + .join(''); + const thinkText = fragments + .filter(isReasoningFragment) + .map(f => f.content) + .join(''); + return { responseText, thinkText }; +} + +function applyResponsePatchOperations(ops, appendFragments) { + if (!Array.isArray(ops)) return false; + let applied = false; + for (const op of ops) { + if (!op || typeof op !== 'object') continue; + if (op.p === 'fragments' && op.o === 'APPEND' && op.v !== undefined) { + appendFragments(op.v); + applied = true; + } + } + return applied; +} + function resolveModelConfig(model) { const requested = String(model || 'deepseek-chat').toLowerCase(); return MODEL_CONFIGS[requested] || MODEL_CONFIGS['deepseek-chat']; @@ -1460,6 +1503,40 @@ const server = http.createServer(async (req, res) => { ? String(requestedSession) : ((remoteAddr === '127.0.0.1' || remoteAddr === '::1' || remoteAddr === '::ffff:127.0.0.1') ? 'dev-agent' : remoteAddr); const agentTag = `[${agentId}]`; + + // "/new" command: if the latest user message is exactly "/new" (whitespace-insensitive), + // reset this agent's DeepSeek session/history instead of forwarding anything to DeepSeek. + const lastUserMessage = [...messages].reverse().find(m => m && m.role === 'user'); + const lastUserText = lastUserMessage && typeof lastUserMessage.content === 'string' + ? lastUserMessage.content.trim() + : ''; + if (lastUserText === '/new') { + const existing = sessions.get(agentId); + const historyCount = existing ? existing.history.length : 0; + sessions.set(agentId, createSession()); + console.log(`${agentTag} /new received — session reset (history cleared: ${historyCount})`); + const confirmation = buildTextResponse('Started a new chat. Session and history have been reset.', '/new', requestedModel); + if (stream) { + if (apiMode === 'anthropic') { + sendAnthropicStream(res, confirmation); + } else if (apiMode === 'responses') { + sendResponsesStream(res, confirmation); + } else { + sendOpenAIStream(res, confirmation); + } + } else { + res.writeHead(200, { 'Content-Type': 'application/json' }); + if (apiMode === 'anthropic') { + res.end(JSON.stringify(toAnthropicResponse(confirmation))); + } else if (apiMode === 'responses') { + res.end(JSON.stringify(toResponsesResponse(confirmation))); + } else { + res.end(JSON.stringify(confirmation)); + } + } + return; + } + const { prompt, systemPrompt } = formatMessages(messages, tools); const session = getOrCreateAgentSession(agentId); @@ -1492,15 +1569,8 @@ const server = http.createServer(async (req, res) => { let finishReason = null; let modelError = null; - const rebuildFragmentText = () => { - const responseText = fragments - .filter(f => f && f.type === 'RESPONSE' && typeof f.content === 'string') - .map(f => f.content) - .join(''); - const thinkText = fragments - .filter(f => f && (f.type === 'THINK' || f.type === 'REASONING') && typeof f.content === 'string') - .map(f => f.content) - .join(''); + const rebuildFragmentState = () => { + const { responseText, thinkText } = rebuildFragmentText(fragments); if (responseText) fullContent = responseText; reasoningContent = thinkText; }; @@ -1510,7 +1580,7 @@ const server = http.createServer(async (req, res) => { for (const fragment of incoming) { if (fragment && typeof fragment === 'object') fragments.push({ ...fragment }); } - rebuildFragmentText(); + rebuildFragmentState(); }; for await (const chunk of readable) { @@ -1522,9 +1592,11 @@ const server = http.createServer(async (req, res) => { try { const d = JSON.parse(line.slice(6)); if (d.response_message_id !== undefined && !newMessageId) newMessageId = d.response_message_id; - if (d.type === 'error' || d.finish_reason || d.content) { + if (isDeepSeekModelErrorEvent(d)) { modelError = { type: d.type || 'error', content: d.content || '', finish_reason: d.finish_reason || null }; - if (d.finish_reason) finishReason = d.finish_reason; + } + if (d.finish_reason) { + finishReason = d.finish_reason; } if (d.p !== undefined) lastPath = d.p; if (d.v && typeof d.v === 'object' && d.v.response) { @@ -1545,11 +1617,14 @@ const server = http.createServer(async (req, res) => { if (lastPath === 'response/fragments' && d.v !== undefined) { appendFragments(d.v); } + if (lastPath === 'response' && d.v !== undefined) { + applyResponsePatchOperations(d.v, appendFragments); + } if (lastPath === 'response/fragments/-1/content' && d.v !== undefined && typeof d.v !== 'object') { if (fragments.length > 0) { const lastFragment = fragments[fragments.length - 1]; lastFragment.content = `${lastFragment.content || ''}${d.v}`; - rebuildFragmentText(); + rebuildFragmentState(); } } if (lastPath === 'response/content' && d.v !== undefined && typeof d.v !== 'object') { @@ -1739,11 +1814,11 @@ async function runAuthScript() { function printStatus() { console.log(`\n${formatWatermark()}`); - console.log(`Auth: ${hasAuthConfig() ? '✅ OK' : '❌ не найден deepseek-auth.json'}`); + console.log(`Auth: ${hasAuthConfig() ? '✅ OK' : '❌ deepseek-auth.json not found'}`); console.log(`Auth source: ${process.env.DEEPSEEK_AUTH_DIR || DS_CONFIG_PATH}`); - console.log(`Аккаунты: ${accounts.length ? accounts.map(a => `${a.id}${a.cooldownUntil > Date.now() ? ' (cooldown)' : ''}`).join(', ') : 'нет'}`); - console.log(`Рабочие модели: ${SUPPORTED_MODEL_IDS.join(', ')}`); - console.log('Нерабочие/скрытые aliases: ' + Object.keys(MODEL_CONFIGS).filter(id => !MODEL_CONFIGS[id].supported).join(', ')); + console.log(`Accounts: ${accounts.length ? accounts.map(a => `${a.id}${a.cooldownUntil > Date.now() ? ' (cooldown)' : ''}`).join(', ') : 'none'}`); + console.log(`Working models: ${SUPPORTED_MODEL_IDS.join(', ')}`); + console.log('Unsupported/hidden aliases: ' + Object.keys(MODEL_CONFIGS).filter(id => !MODEL_CONFIGS[id].supported).join(', ')); console.log('Capabilities: GET /v1/model-capabilities'); } @@ -1754,14 +1829,14 @@ async function showStartupMenu() { } while (true) { printStatus(); - console.log('\n=== Меню ==='); + console.log('\n=== Menu ==='); console.log(`ForgetMeAI: ${FORGETMEAI_WATERMARK}`); - console.log('1 - Авторизоваться / обновить DeepSeek login'); - console.log('2 - Импортировать auth-файл / cookies'); - console.log('3 - Показать модели и статусы'); - console.log('4 - Запустить прокси (по умолчанию)'); - console.log('5 - Выход'); - let choice = await prompt('Ваш выбор (Enter = 4): '); + console.log('1 - Authorize / update DeepSeek login'); + console.log('2 - Import auth file / cookies'); + console.log('3 - Show models and statuses'); + console.log('4 - Start proxy (default)'); + console.log('5 - Exit'); + let choice = await prompt('Your choice (Enter = 4): '); if (!choice) choice = '4'; if (choice === '1') { await runAuthScript(); @@ -1770,10 +1845,10 @@ async function showStartupMenu() { loadDeepSeekConfig({ fatal: false }); } else if (choice === '3') { console.log(JSON.stringify(ALL_MODEL_CAPABILITIES, null, 2)); - await prompt('\nНажмите Enter, чтобы вернуться в меню...'); + await prompt('\nPress Enter to return to the menu...'); } else if (choice === '4') { if (!hasAuthConfig()) { - console.log('Нужен deepseek-auth.json. Запустите пункт 1 или 2.'); + console.log('deepseek-auth.json required. Run option 1 or 2 first.'); continue; } return true; @@ -1801,4 +1876,16 @@ async function main() { }); } -main().catch(err => { console.error('[DS-API] FATAL:', err); process.exit(1); }); +if (require.main === module) { + main().catch(err => { console.error('[DS-API] FATAL:', err); process.exit(1); }); +} + +module.exports = { + __test: { + isAssistantOutputFragment, + isReasoningFragment, + isDeepSeekModelErrorEvent, + rebuildFragmentText, + applyResponsePatchOperations, + }, +}; diff --git a/tests/unit.test.js b/tests/unit.test.js index 31a9656..a02bc23 100644 --- a/tests/unit.test.js +++ b/tests/unit.test.js @@ -6,6 +6,7 @@ const path = require('node:path'); const { spawnSync } = require('node:child_process'); const ROOT = path.resolve(__dirname, '..'); +const serverInternals = require('../server.js').__test; function tmpdir() { return fs.mkdtempSync(path.join(os.tmpdir(), 'fdsapi-test-')); @@ -109,3 +110,36 @@ test('chrome auth prints actionable OS instructions when Chrome is missing', () assert.match(out, /Linux/i); assert.match(out, /CHROME_PATH/i); }); + +test('DeepSeek stream parser treats SEARCH fragments as assistant output', () => { + const rebuilt = serverInternals.rebuildFragmentText([ + { type: 'SEARCH', content: 'The official Reuters website is ' }, + { type: 'SEARCH', content: 'https://www.reuters.com/.' }, + ]); + + assert.equal(rebuilt.responseText, 'The official Reuters website is https://www.reuters.com/.'); + assert.equal(rebuilt.thinkText, ''); +}); + +test('DeepSeek stream parser applies response-level fragment append patches', () => { + const fragments = []; + const appendFragments = (value) => { + const incoming = Array.isArray(value) ? value : [value]; + for (const fragment of incoming) fragments.push({ ...fragment }); + }; + + const applied = serverInternals.applyResponsePatchOperations([ + { p: 'fragments', o: 'APPEND', v: [{ type: 'RESPONSE', content: 'The' }] }, + { p: 'has_pending_fragment', o: 'SET', v: false }, + ], appendFragments); + + assert.equal(applied, true); + assert.deepEqual(fragments, [{ type: 'RESPONSE', content: 'The' }]); + assert.equal(serverInternals.rebuildFragmentText(fragments).responseText, 'The'); +}); + +test('DeepSeek stream parser does not treat service content chunks as model errors', () => { + assert.equal(serverInternals.isDeepSeekModelErrorEvent({ content: 'Official Reuters website URL' }), false); + assert.equal(serverInternals.isDeepSeekModelErrorEvent({ finish_reason: 'stop' }), false); + assert.equal(serverInternals.isDeepSeekModelErrorEvent({ type: 'error', content: 'backend error' }), true); +});