From c49b1eae54cb87d4f8f5b8ec713a770b787b17a2 Mon Sep 17 00:00:00 2001 From: dansc Date: Sun, 7 Jun 2026 11:11:52 +0300 Subject: [PATCH 01/10] feat: built-in web dashboard (chat + overview) Add GET /dashboard (serves public/dashboard.html) and GET /api/auth-status (JWT exp decode + presence flags) to server.js. New single-file dashboard: streaming chat with markdown (marked+DOMPurify via CDN with SRI), DeepSeek reasoning panel (reasoning_content), web-search toggle, model selector; overview tab with connection info (cURL/Python), model list with capability badges, and auth status. No new runtime deps. Co-Authored-By: Claude Opus 4.8 --- public/dashboard.html | 416 ++++++++++++++++++++++++++++++++++++++++++ server.js | 31 ++++ 2 files changed, 447 insertions(+) create mode 100644 public/dashboard.html diff --git a/public/dashboard.html b/public/dashboard.html new file mode 100644 index 0000000..757062d --- /dev/null +++ b/public/dashboard.html @@ -0,0 +1,416 @@ + + + + + +FreeDeepseekAPI — Дашборд + + + + + + + + +
+
+

FreeDeepseekAPI

+ проверка… +
+ + + + +
+
+

Плейграунд чата +
+ + + +
+

+
+
+ + +
+
+
+ + +
+
+
статус
+
моделей
+
авторизация
+
+
+
+

Как подключить

+
+
любой непустой (sk-deepseek)
+
deepseek-chat
+
+ + +
+
OpenAI-совместимо. Также есть Anthropic-шим /v1/messages и Responses /v1/responses.
+
+
+

Авторизация DeepSeek

+
загрузка…
+
Обновить логин: запустите Авторизация DeepSeek.bat, войдите в DeepSeek в открывшемся окне, отправьте ok и нажмите Enter в терминале.
+
+
+

Модели (0) + R reasoning · S web-поиск · клик копирует

+
загрузка…
+
+
+
+ + +
+
+ + + + diff --git a/server.js b/server.js index 190f740..3180031 100755 --- a/server.js +++ b/server.js @@ -1021,6 +1021,37 @@ const server = http.createServer(async (req, res) => { return; } + // Dashboard (web UI) — single static file + if (req.method === 'GET' && url.pathname === '/dashboard') { + try { + const html = fs.readFileSync(path.join(__dirname, 'public', 'dashboard.html')); + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(html); + } catch { + res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); + res.end('Dashboard not built (public/dashboard.html missing)'); + } + return; + } + + // Auth status for dashboard: decode JWT exp (no signature check) + presence flags + if (req.method === 'GET' && url.pathname === '/api/auth-status') { + let tokenExp = null; + try { + const payload = JSON.parse(Buffer.from(String(DS_CONFIG.token || '').split('.')[1], 'base64url').toString()); + if (payload.exp) tokenExp = payload.exp * 1000; + } catch { /* token missing or not a JWT */ } + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + config_ready: hasAuthConfig(), + token_exp: tokenExp, + has_token: !!DS_CONFIG.token, + has_cookie: !!DS_CONFIG.cookie, + has_hif: !!(DS_CONFIG.hif_dliq || DS_CONFIG.hif_leim), + })); + return; + } + const apiMode = url.pathname === '/v1/messages' ? 'anthropic' : (url.pathname === '/v1/responses' ? 'responses' : 'openai'); From 5392ed992db5b00270da9a3d26c60c1810efc52c Mon Sep 17 00:00:00 2001 From: dansc Date: Sun, 7 Jun 2026 12:35:15 +0300 Subject: [PATCH 02/10] feat: import auth from browser cURL (cross-browser, no isolated profile) Add scripts/auth_from_curl.js: parses a 'Copy as cURL' request (Chrome/Firefox/Edge, single or double quotes) and writes deepseek-auth.json (token/cookie/hif). Lets users authorize from THEIR own logged-in browser instead of the isolated Chrome-for-Testing profile (which has no Google accounts) and works in Firefox too. Co-Authored-By: Claude Opus 4.8 --- scripts/auth_from_curl.js | 91 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 scripts/auth_from_curl.js diff --git a/scripts/auth_from_curl.js b/scripts/auth_from_curl.js new file mode 100644 index 0000000..85c50e8 --- /dev/null +++ b/scripts/auth_from_curl.js @@ -0,0 +1,91 @@ +#!/usr/bin/env node +/* + Импорт авторизации DeepSeek из "Copy as cURL" (Chrome / Firefox / Edge). + Работает с любым браузером, где вы залогинены в chat.deepseek.com — + ничего вводить вручную не нужно, берём готовые заголовки запроса. + + Использование: + node scripts/auth_from_curl.js < curl.txt # из файла через stdin + node scripts/auth_from_curl.js path/to/curl.txt # из файла-аргумента + Get-Clipboard -Raw | node scripts/auth_from_curl.js # из буфера обмена (PowerShell) + + Как получить cURL: + 1. Откройте chat.deepseek.com в своём браузере (вы должны быть залогинены). + 2. F12 -> вкладка Network. + 3. Отправьте в DeepSeek любое сообщение (например: ok). + 4. Правый клик на запросе к chat.deepseek.com/api/... -> Copy -> Copy as cURL. +*/ +const fs = require('fs'); +const path = require('path'); + +function readStdin() { + return new Promise(resolve => { + let s = ''; + process.stdin.setEncoding('utf8'); + process.stdin.on('data', d => s += d); + process.stdin.on('end', () => resolve(s)); + // если stdin не подключён (TTY) — не зависаем + if (process.stdin.isTTY) resolve(''); + }); +} + +// Извлекает заголовки из cURL: -H 'name: value' | -H "name: value" | --header ... +function extractHeaders(curl) { + const headers = {}; + const re = /(?:-H|--header)\s+(['"])(.+?):\s?([\s\S]*?)\1(?=\s|$)/g; + let m; + while ((m = re.exec(curl))) { + headers[m[2].trim().toLowerCase()] = m[3].trim(); + } + // cookie иногда приходит отдельным флагом -b/--cookie + if (!headers['cookie']) { + const mc = curl.match(/(?:-b|--cookie)\s+(['"])([\s\S]*?)\1(?=\s|$)/); + if (mc) headers['cookie'] = mc[2].trim(); + } + return headers; +} + +(async () => { + const argFile = process.argv[2]; + let curl = ''; + if (argFile && fs.existsSync(argFile)) curl = fs.readFileSync(argFile, 'utf8'); + else curl = await readStdin(); + curl = String(curl || '').trim(); + + if (!curl) { + console.error('Пусто: передайте cURL через stdin, файл-аргумент или буфер обмена.'); + process.exit(1); + } + if (!/deepseek\.com/i.test(curl)) { + console.error('[!] В cURL не видно deepseek.com — похоже, скопирован не тот запрос. Продолжаю на всякий случай...'); + } + + const h = extractHeaders(curl); + const token = (h['authorization'] || '').replace(/^Bearer\s+/i, '').trim(); + const cookie = h['cookie'] || ''; + const hif_dliq = h['x-hif-dliq'] || ''; + const hif_leim = h['x-hif-leim'] || ''; + + const missing = []; + if (!token) missing.push('token (заголовок authorization: Bearer ...)'); + if (!cookie) missing.push('cookie'); + if (missing.length) { + console.error('Не удалось извлечь: ' + missing.join(', ')); + console.error('Убедитесь, что скопировали запрос к chat.deepseek.com/api/... через "Copy as cURL".'); + process.exit(2); + } + + const out = { + token, + hif_dliq, + hif_leim, + cookie, + wasmUrl: 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm', + }; + const dest = process.env.DEEPSEEK_AUTH_PATH || path.join(__dirname, '..', 'deepseek-auth.json'); + fs.writeFileSync(dest, JSON.stringify(out, null, 2)); + console.log('OK -> ' + dest); + console.log(' token: ' + token.length + ' символов'); + console.log(' cookie: ' + cookie.split(';').filter(Boolean).length + ' значений'); + console.log(' hif: ' + ((hif_dliq || hif_leim) ? 'захвачены' : 'нет (опционально)')); +})(); From 4e2af25b3d1c97077a90249e77a1a94677383dee Mon Sep 17 00:00:00 2001 From: dansc Date: Sun, 7 Jun 2026 13:05:40 +0300 Subject: [PATCH 03/10] feat: import auth from HAR file (Save All As HAR) Add scripts/auth_from_har.js: parses a full HAR export, auto-picks the best chat.deepseek.com/api request with Authorization: Bearer, extracts token/cookie/hif and real wasmUrl into deepseek-auth.json. Complements the cURL importer for users who export HAR from DevTools. Co-Authored-By: Claude Opus 4.8 --- scripts/auth_from_har.js | 78 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 scripts/auth_from_har.js diff --git a/scripts/auth_from_har.js b/scripts/auth_from_har.js new file mode 100644 index 0000000..d1b220a --- /dev/null +++ b/scripts/auth_from_har.js @@ -0,0 +1,78 @@ +#!/usr/bin/env node +/* + Импорт авторизации DeepSeek из HAR-файла. + HAR = DevTools -> Network -> правый клик -> "Save all as HAR" (или иконка экспорта). + Работает с любым браузером (Chrome/Firefox/Edge), где вы залогинены в DeepSeek. + + Использование: + node scripts/auth_from_har.js "path/to/archive.har" + + Скрипт сам выбирает лучший запрос к chat.deepseek.com/api/... с заголовком + Authorization: Bearer и извлекает token/cookie/hif/wasmUrl -> deepseek-auth.json. +*/ +const fs = require('fs'); +const path = require('path'); + +const harPath = process.argv[2]; +if (!harPath || !fs.existsSync(harPath)) { + console.error('Укажите путь к .har: node scripts/auth_from_har.js "archive.har"'); + process.exit(1); +} +let har; +try { har = JSON.parse(fs.readFileSync(harPath, 'utf8')); } +catch (e) { console.error('Не удалось прочитать HAR (не JSON): ' + e.message); process.exit(1); } + +const entries = (har.log && har.log.entries) || []; +const hv = (headers, name) => { + const h = (headers || []).find(x => (x.name || '').toLowerCase() === name); + return h ? (h.value || '') : ''; +}; + +// Выбираем лучший запрос: к deepseek, с Authorization: Bearer, по сумме полезных заголовков +let best = null; +for (const e of entries) { + const req = e.request || {}; + const url = req.url || ''; + if (!/deepseek\.com/i.test(url)) continue; + const auth = hv(req.headers, 'authorization'); + if (!/bearer\s+\S/i.test(auth)) continue; + const cookie = hv(req.headers, 'cookie'); + const dliq = hv(req.headers, 'x-hif-dliq'); + const leim = hv(req.headers, 'x-hif-leim'); + const score = (cookie ? 2 : 0) + (dliq ? 1 : 0) + (leim ? 1 : 0) + (/\/api\//.test(url) ? 1 : 0); + if (!best || score > best.score) best = { url, headers: req.headers, auth, cookie, dliq, leim, score }; +} + +if (!best) { + console.error('В HAR не найдено запросов к deepseek.com с заголовком Authorization: Bearer.'); + console.error('Сохраняйте HAR уже залогиненным и после отправки сообщения в DeepSeek.'); + process.exit(2); +} + +const token = best.auth.replace(/^Bearer\s+/i, '').trim(); +const cookie = best.cookie; +const hif_dliq = best.dliq; +const hif_leim = best.leim; + +// wasmUrl: ищем реальный запрос к sha3*.wasm в HAR, иначе дефолт +let wasmUrl = ''; +for (const e of entries) { + const u = (e.request && e.request.url) || ''; + if (/sha3.*\.wasm/i.test(u)) { wasmUrl = u; break; } +} +if (!wasmUrl) wasmUrl = 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm'; + +if (!token || !cookie) { + console.error('Не хватает данных: ' + (!token ? 'token ' : '') + (!cookie ? 'cookie' : '')); + process.exit(2); +} + +const out = { token, hif_dliq, hif_leim, cookie, wasmUrl }; +const dest = process.env.DEEPSEEK_AUTH_PATH || path.join(__dirname, '..', 'deepseek-auth.json'); +fs.writeFileSync(dest, JSON.stringify(out, null, 2)); +console.log('OK -> ' + dest); +console.log(' source: ' + best.url.slice(0, 72)); +console.log(' token: ' + token.length + ' символов'); +console.log(' cookie: ' + cookie.split(';').filter(Boolean).length + ' значений'); +console.log(' hif: ' + ((hif_dliq || hif_leim) ? 'захвачены' : 'нет (опционально)')); +console.log(' wasmUrl: ' + (/^https/.test(wasmUrl) ? 'ok' : 'default')); From 6467b90c4a2f945117be1584486f05d2dfa762c9 Mon Sep 17 00:00:00 2001 From: dansc Date: Sun, 7 Jun 2026 13:32:00 +0300 Subject: [PATCH 04/10] fix: disable broken web-search, harden wasmUrl fallback Web-search (deepseek-chat-search) is removed from the dashboard model controls: DeepSeek Web currently returns an empty response for search-enabled requests (server retries 10x then gives up). Chat picks the model directly from the selector now. auth_from_curl/har: keep the working wasmUrl from the previous deepseek-auth.json instead of always falling back to a hardcoded (eventually stale) default. Co-Authored-By: Claude Opus 4.8 --- public/dashboard.html | 24 ++++-------------------- scripts/auth_from_curl.js | 12 +++++------- scripts/auth_from_har.js | 8 ++++++++ 3 files changed, 17 insertions(+), 27 deletions(-) diff --git a/public/dashboard.html b/public/dashboard.html index 757062d..db63351 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -181,7 +181,6 @@

Плейграунд чата deepseek-expert -

@@ -316,23 +315,8 @@

Модели (Модели (Модели ( ' + dest); console.log(' token: ' + token.length + ' символов'); diff --git a/scripts/auth_from_har.js b/scripts/auth_from_har.js index d1b220a..d0bb83c 100644 --- a/scripts/auth_from_har.js +++ b/scripts/auth_from_har.js @@ -60,6 +60,14 @@ for (const e of entries) { const u = (e.request && e.request.url) || ''; if (/sha3.*\.wasm/i.test(u)) { wasmUrl = u; break; } } +if (!wasmUrl) { + // не нашли в HAR — берём рабочий из прошлого auth, чтобы не подставлять устаревший дефолт + try { + const dp = process.env.DEEPSEEK_AUTH_PATH || path.join(__dirname, '..', 'deepseek-auth.json'); + const old = JSON.parse(fs.readFileSync(dp, 'utf8')); + if (old.wasmUrl) wasmUrl = old.wasmUrl; + } catch { /* нет прошлого файла */ } +} if (!wasmUrl) wasmUrl = 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm'; if (!token || !cookie) { From a2fe502d948f5d2fbd2653c490b59dd02a2f83e5 Mon Sep 17 00:00:00 2001 From: dansc Date: Sun, 7 Jun 2026 18:13:32 +0300 Subject: [PATCH 05/10] feat: multi-account pool with automatic rotation on rate limits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add accountManager.js (round-robin pool in deepseek-accounts.json, migrates from single deepseek-auth.json) and lib/parseAuth.js (shared cURL/HAR parser). server.js: per-account headers/POW, askWithRotation wrapper that rotates on HTTP 429 (rate limit тЖТ cooldown), 401/403 (invalid), request errors, and hidden-limit empty responses; returns 503 when all accounts exhausted. New localhost-gated endpoints GET /api/accounts, POST /api/accounts/import (cURL/HAR), POST /:id/check, DELETE /:id. Dashboard: Accounts tab (status bars, import, check/delete). Auth scripts now add to the pool. Cooldown via DEEPSEEK_RATELIMIT_HOURS (default 6). Co-Authored-By: Claude Opus 4.8 --- .gitignore | 2 + accountManager.js | 128 +++++++++++++++++++++ lib/parseAuth.js | 100 ++++++++++++++++ public/dashboard.html | 78 ++++++++++++- scripts/auth_from_curl.js | 89 ++++----------- scripts/auth_from_har.js | 82 +++----------- server.js | 233 ++++++++++++++++++++++++++++++-------- 7 files changed, 523 insertions(+), 189 deletions(-) create mode 100644 accountManager.js create mode 100644 lib/parseAuth.js diff --git a/.gitignore b/.gitignore index 2610c6d..a383ddf 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ deepseek-auth.json /tmp/deepseek_response_* .DS_Store *.pdf + +deepseek-accounts.json diff --git a/accountManager.js b/accountManager.js new file mode 100644 index 0000000..18e62a0 --- /dev/null +++ b/accountManager.js @@ -0,0 +1,128 @@ +'use strict'; +/* + Пул аккаунтов DeepSeek с round-robin и пометкой лимитов/инвалида. + Аналог FreeQwenApi/src/api/tokenManager.js, адаптирован под плоскую + структуру DeepSeek (без браузерных профилей на аккаунт). + + Хранилище: deepseek-accounts.json — массив + { id, token, cookie, hif_dliq, hif_leim, wasmUrl, resetAt, invalid } + Миграция: если файла нет, но есть deepseek-auth.json (один аккаунт) — + создаём пул из одного acc_1 (обратная совместимость). +*/ +const fs = require('fs'); +const path = require('path'); + +const ACCOUNTS_PATH = process.env.DEEPSEEK_ACCOUNTS_PATH || path.join(__dirname, 'deepseek-accounts.json'); +const AUTH_PATH = process.env.DEEPSEEK_AUTH_PATH || path.join(__dirname, 'deepseek-auth.json'); +const COOLDOWN_HOURS = Number(process.env.DEEPSEEK_RATELIMIT_HOURS || 6); + +let pointer = 0; + +function decodeTokenInfo(token) { + try { + const p = JSON.parse(Buffer.from(String(token).split('.')[1], 'base64url').toString()); + return { exp: p.exp ? p.exp * 1000 : null }; + } catch { return { exp: null }; } +} + +function saveAccounts(accounts) { + try { fs.writeFileSync(ACCOUNTS_PATH, JSON.stringify(accounts, null, 2), 'utf8'); } + catch (e) { console.error('[accounts] ошибка сохранения:', e.message); } +} + +function loadAccounts() { + if (fs.existsSync(ACCOUNTS_PATH)) { + try { const a = JSON.parse(fs.readFileSync(ACCOUNTS_PATH, 'utf8')); if (Array.isArray(a)) return a; } + catch (e) { console.error('[accounts] ошибка чтения deepseek-accounts.json:', e.message); } + } + // миграция из единственного deepseek-auth.json + if (fs.existsSync(AUTH_PATH)) { + try { + const one = JSON.parse(fs.readFileSync(AUTH_PATH, 'utf8')); + if (one && one.token) { + const acc = [{ + id: 'acc_1', token: one.token, cookie: one.cookie || '', + hif_dliq: one.hif_dliq || '', hif_leim: one.hif_leim || '', + wasmUrl: one.wasmUrl || '', resetAt: null, invalid: false, + }]; + saveAccounts(acc); + console.log('[accounts] миграция deepseek-auth.json -> deepseek-accounts.json (acc_1)'); + return acc; + } + } catch (e) { console.error('[accounts] ошибка миграции:', e.message); } + } + return []; +} + +function isExpired(a) { const { exp } = decodeTokenInfo(a.token); return exp ? exp <= Date.now() : false; } + +function isUsable(a, now) { + return !a.invalid && (!a.resetAt || Date.parse(a.resetAt) <= now) && !isExpired(a); +} + +function getAvailableAccount() { + const accounts = loadAccounts(); + const now = Date.now(); + const valid = accounts.filter(a => isUsable(a, now)); + if (!valid.length) return null; + const acc = valid[pointer % valid.length]; + pointer = (pointer + 1) % valid.length; + return acc; +} + +function hasValidAccounts() { + const now = Date.now(); + return loadAccounts().some(a => isUsable(a, now)); +} +function hasAnyAccount() { return loadAccounts().length > 0; } +function listAccounts() { return loadAccounts(); } +function getAccountById(id) { return loadAccounts().find(a => a.id === id) || null; } + +function _update(id, fn) { + const a = loadAccounts(); + const i = a.findIndex(x => x.id === id); + if (i < 0) return false; + fn(a[i]); saveAccounts(a); return true; +} +function markRateLimited(id, hours) { + const h = Number(hours) || COOLDOWN_HOURS; + return _update(id, a => { a.resetAt = new Date(Date.now() + h * 3600 * 1000).toISOString(); }); +} +function markInvalid(id) { return _update(id, a => { a.invalid = true; }); } +function markValid(id) { return _update(id, a => { a.invalid = false; a.resetAt = null; }); } + +function addAccount(obj) { + if (!obj || !obj.token) return { error: 'Нужен token' }; + if (!obj.cookie) return { error: 'Нужен cookie' }; + const a = loadAccounts(); + if (a.some(x => x.token === obj.token)) return { error: 'Этот аккаунт уже добавлен' }; + let n = 1; const ids = new Set(a.map(x => x.id)); + while (ids.has('acc_' + n)) n++; + const id = 'acc_' + n; + a.push({ + id, token: obj.token, cookie: obj.cookie, + hif_dliq: obj.hif_dliq || '', hif_leim: obj.hif_leim || '', + wasmUrl: obj.wasmUrl || '', resetAt: null, invalid: false, + }); + saveAccounts(a); + const { exp } = decodeTokenInfo(obj.token); + return { ok: true, id, exp }; +} + +function deleteAccount(id) { + if (typeof id !== 'string' || !/^acc_[a-zA-Z0-9]+$/.test(id)) return { error: 'Некорректный id аккаунта' }; + const a = loadAccounts(); + const next = a.filter(x => x.id !== id); + if (next.length === a.length) return { error: 'Аккаунт не найден' }; + saveAccounts(next); + return { ok: true }; +} + +// первый известный рабочий wasmUrl — чтобы подставлять при импорте без wasm в cURL +function anyWasmUrl() { const a = loadAccounts().find(x => x.wasmUrl); return a ? a.wasmUrl : ''; } + +module.exports = { + loadAccounts, saveAccounts, listAccounts, getAvailableAccount, getAccountById, + hasValidAccounts, hasAnyAccount, markRateLimited, markInvalid, markValid, + addAccount, deleteAccount, decodeTokenInfo, anyWasmUrl, COOLDOWN_HOURS, +}; diff --git a/lib/parseAuth.js b/lib/parseAuth.js new file mode 100644 index 0000000..41320be --- /dev/null +++ b/lib/parseAuth.js @@ -0,0 +1,100 @@ +'use strict'; +/* + Общий парсер авторизации DeepSeek из "Copy as cURL" или HAR-файла. + Используется и CLI-скриптами (scripts/auth_from_*.js), и эндпоинтом + дашборда POST /api/accounts/import. Возвращает плоский объект + { token, cookie, hif_dliq, hif_leim, wasmUrl } или { error }. +*/ + +const WASM_DEFAULT = 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm'; + +// -H 'name: value' | -H "name: value" | --header ... +function extractHeadersFromCurl(curl) { + const headers = {}; + const re = /(?:-H|--header)\s+(['"])(.+?):\s?([\s\S]*?)\1(?=\s|$)/g; + let m; + while ((m = re.exec(curl))) headers[m[2].trim().toLowerCase()] = m[3].trim(); + if (!headers['cookie']) { + const mc = curl.match(/(?:-b|--cookie)\s+(['"])([\s\S]*?)\1(?=\s|$)/); + if (mc) headers['cookie'] = mc[2].trim(); + } + return headers; +} + +function fromHeaders(h) { + return { + token: (h['authorization'] || '').replace(/^Bearer\s+/i, '').trim(), + cookie: h['cookie'] || '', + hif_dliq: h['x-hif-dliq'] || '', + hif_leim: h['x-hif-leim'] || '', + }; +} + +function parseCurl(curl) { + const s = String(curl || ''); + const r = fromHeaders(extractHeadersFromCurl(s)); + const wm = s.match(/https?:\/\/[^\s'"]*sha3[^\s'"]*\.wasm/i); + r.wasmUrl = wm ? wm[0] : ''; + return r; +} + +function parseHar(harText) { + let har; + try { har = (typeof harText === 'object') ? harText : JSON.parse(harText); } + catch { return { error: 'Не удалось прочитать HAR (не 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 + let best = null; + for (const e of entries) { + const req = e.request || {}; + const url = req.url || ''; + if (!/deepseek\.com/i.test(url)) continue; + const auth = hv(req.headers, 'authorization'); + if (!/bearer\s+\S/i.test(auth)) continue; + const cookie = hv(req.headers, 'cookie'); + const dliq = hv(req.headers, 'x-hif-dliq'); + const leim = hv(req.headers, 'x-hif-leim'); + const score = (cookie ? 2 : 0) + (dliq ? 1 : 0) + (leim ? 1 : 0) + (/\/api\//.test(url) ? 1 : 0); + if (!best || score > best.score) { + 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' }; + + 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). +function parseAuthInput(text) { + const s = String(text || '').trim(); + if (!s) return { error: 'Пустой ввод' }; + if (s[0] === '{' || s[0] === '[') { + const r = parseHar(s); + if (!r.error) return r; // это был HAR + } + if (/\bcurl\b|--header|(^|\s)-H\s/i.test(s)) return parseCurl(s); + // последняя попытка — вдруг HAR без явного префикса + return parseHar(s); +} + +// Валидация + проставление wasmUrl (из ввода → из прошлого аккаунта → дефолт). +function finalizeAuth(parsed, prevWasmUrl) { + if (!parsed || parsed.error) return parsed || { error: 'Пусто' }; + const missing = []; + if (!parsed.token) missing.push('token (authorization: Bearer)'); + if (!parsed.cookie) missing.push('cookie'); + if (missing.length) return { error: 'Не найдено: ' + missing.join(', ') }; + return { + token: parsed.token, + hif_dliq: parsed.hif_dliq || '', + hif_leim: parsed.hif_leim || '', + cookie: parsed.cookie, + wasmUrl: parsed.wasmUrl || prevWasmUrl || WASM_DEFAULT, + }; +} + +module.exports = { parseCurl, parseHar, parseAuthInput, finalizeAuth, WASM_DEFAULT }; diff --git a/public/dashboard.html b/public/dashboard.html index db63351..8725ae3 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -90,6 +90,18 @@ .models{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:6px;max-height:280px;overflow:auto;padding-right:4px} .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)} + + /* аккаунты — прямые полосы статуса */ + .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)} + .acc.INVALID,.acc.EXPIRED,.acc.ERROR{border-left-color:var(--err)} + .acc .id{font-weight:600;font-size:var(--fs-sm)} .acc .meta{color:var(--muted);font-size:var(--fs-xs);font-family:var(--mono)} + .badge{font-size:var(--fs-xs);font-weight:700;padding:3px 9px;border-radius:4px;display:inline-flex;align-items:center;gap:5px} + .badge::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor} + .badge.OK{background:rgba(63,185,80,.16);color:#56d364} .badge.WAIT{background:rgba(210,153,34,.18);color:#e3b341} + .badge.INVALID,.badge.EXPIRED,.badge.ERROR{background:rgba(248,81,73,.2);color:#ff7b72} .badge.checking{background:rgba(77,107,254,.16);color:var(--accent)} + .import-box{margin-top:var(--s4);border-top:1px solid var(--border);padding-top:var(--s4)} .chip .id{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1} .chip .caps{display:flex;gap:3px;flex-shrink:0} .cap{font-size:9px;font-weight:700;padding:1px 4px;border-radius:3px;letter-spacing:.02em} @@ -157,6 +169,8 @@ + +
@@ -167,6 +181,7 @@

FreeDeepseekAPI

@@ -192,12 +207,26 @@

Плейграунд чата + +
+
+

Аккаунты DeepSeek +

+
загрузка…
+
+
Добавить аккаунт: в браузере (где вошли в DeepSeek) F12 → Network → отправьте сообщение → правый клик на запросе к chat.deepseek.com/api/...Copy as cURL (или Save All As HAR) → вставьте сюда.
+ +
+
+
+
+
статус
моделей
-
авторизация
+
аккаунтов онлайн
@@ -246,7 +275,7 @@

Модели (Модели (Модели (Модели (
${j.has_hif?'есть ✓':'опционально'}

${exp}`; }catch{ box.innerHTML='
Не удалось получить статус авторизации
'; } } +// ── Аккаунты ─────────────────────────────────────────────────────────────── +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 ''; } } +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; } + list.innerHTML=j.accounts.map(a=>{ + const meta=a.resetAt?('лимит до '+fmtResetAt(a.resetAt)):(a.exp?fmtExp(a.exp):''); + return `
${esc(a.id)}${accStLabel(a.status)}…${esc(a.preview||'')}${esc(meta)} + +
`; + }).join(''); + }catch{ list.innerHTML='
Не удалось загрузить аккаунты
'; } +} +$('#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; +}; +$('#importBtn').onclick=async()=>{ + const text=$('#importText').value.trim(); if(!text) return toast('Вставьте cURL или 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; +}; +document.addEventListener('click', async e=>{ + const t=e.target.closest('button'); if(!t) 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; + try{ await fetch(origin+'/api/accounts/'+encodeURIComponent(t.dataset.check)+'/check',{method:'POST'}); }catch{} await loadAccountsList(); await loadHealth(); return; } + if(t.dataset.del){ if(!confirm('Удалить аккаунт '+t.dataset.del+'?')) return; t.disabled=true; + try{ await fetch(origin+'/api/accounts/'+encodeURIComponent(t.dataset.del),{method:'DELETE'}); toast('Удалён','ok'); }catch{ toast('Ошибка','err'); } loadAccountsList(); loadHealth(); return; } +}); +setInterval(()=>{ if($('#tab-accounts').classList.contains('active')) loadAccountsList(); }, 20000); // ── Модели ───────────────────────────────────────────────────────────────── async function loadModels(){ const list=$('#modelList'); diff --git a/scripts/auth_from_curl.js b/scripts/auth_from_curl.js index 67b1d3c..a1b2952 100644 --- a/scripts/auth_from_curl.js +++ b/scripts/auth_from_curl.js @@ -1,22 +1,19 @@ #!/usr/bin/env node /* - Импорт авторизации DeepSeek из "Copy as cURL" (Chrome / Firefox / Edge). - Работает с любым браузером, где вы залогинены в chat.deepseek.com — - ничего вводить вручную не нужно, берём готовые заголовки запроса. + Добавляет аккаунт DeepSeek из "Copy as cURL" в ПУЛ (deepseek-accounts.json). + Работает с любым браузером, где вы залогинены в chat.deepseek.com. Использование: - node scripts/auth_from_curl.js < curl.txt # из файла через stdin - node scripts/auth_from_curl.js path/to/curl.txt # из файла-аргумента - Get-Clipboard -Raw | node scripts/auth_from_curl.js # из буфера обмена (PowerShell) + node scripts/auth_from_curl.js < curl.txt + node scripts/auth_from_curl.js path/to/curl.txt + Get-Clipboard -Raw | node scripts/auth_from_curl.js # из буфера (PowerShell) - Как получить cURL: - 1. Откройте chat.deepseek.com в своём браузере (вы должны быть залогинены). - 2. F12 -> вкладка Network. - 3. Отправьте в DeepSeek любое сообщение (например: ok). - 4. Правый клик на запросе к chat.deepseek.com/api/... -> Copy -> Copy as cURL. + Как получить cURL: chat.deepseek.com → F12 → Network → отправьте сообщение → + правый клик на запросе к /api/v0/... → Copy → Copy as cURL. */ const fs = require('fs'); -const path = require('path'); +const { parseAuthInput, finalizeAuth } = require('../lib/parseAuth'); +const accounts = require('../accountManager'); function readStdin() { return new Promise(resolve => { @@ -24,66 +21,24 @@ function readStdin() { process.stdin.setEncoding('utf8'); process.stdin.on('data', d => s += d); process.stdin.on('end', () => resolve(s)); - // если stdin не подключён (TTY) — не зависаем if (process.stdin.isTTY) resolve(''); }); } -// Извлекает заголовки из cURL: -H 'name: value' | -H "name: value" | --header ... -function extractHeaders(curl) { - const headers = {}; - const re = /(?:-H|--header)\s+(['"])(.+?):\s?([\s\S]*?)\1(?=\s|$)/g; - let m; - while ((m = re.exec(curl))) { - headers[m[2].trim().toLowerCase()] = m[3].trim(); - } - // cookie иногда приходит отдельным флагом -b/--cookie - if (!headers['cookie']) { - const mc = curl.match(/(?:-b|--cookie)\s+(['"])([\s\S]*?)\1(?=\s|$)/); - if (mc) headers['cookie'] = mc[2].trim(); - } - return headers; -} - (async () => { - const argFile = process.argv[2]; - let curl = ''; - if (argFile && fs.existsSync(argFile)) curl = fs.readFileSync(argFile, 'utf8'); - else curl = await readStdin(); - curl = String(curl || '').trim(); - - if (!curl) { - console.error('Пусто: передайте cURL через stdin, файл-аргумент или буфер обмена.'); - process.exit(1); - } - if (!/deepseek\.com/i.test(curl)) { - console.error('[!] В cURL не видно deepseek.com — похоже, скопирован не тот запрос. Продолжаю на всякий случай...'); - } - - const h = extractHeaders(curl); - const token = (h['authorization'] || '').replace(/^Bearer\s+/i, '').trim(); - const cookie = h['cookie'] || ''; - const hif_dliq = h['x-hif-dliq'] || ''; - const hif_leim = h['x-hif-leim'] || ''; - - const missing = []; - if (!token) missing.push('token (заголовок authorization: Bearer ...)'); - if (!cookie) missing.push('cookie'); - if (missing.length) { - console.error('Не удалось извлечь: ' + missing.join(', ')); - console.error('Убедитесь, что скопировали запрос к chat.deepseek.com/api/... через "Copy as cURL".'); + const arg = process.argv[2]; + let input = (arg && fs.existsSync(arg)) ? fs.readFileSync(arg, 'utf8') : await readStdin(); + input = String(input || '').trim(); + if (!input) { console.error('Пусто: передайте cURL через stdin, файл-аргумент или буфер обмена.'); process.exit(1); } + + const parsed = finalizeAuth(parseAuthInput(input), accounts.anyWasmUrl()); + if (parsed.error) { + console.error('Ошибка: ' + parsed.error); + console.error('Скопируйте именно запрос к chat.deepseek.com/api/... через "Copy as cURL".'); process.exit(2); } - - const dest = process.env.DEEPSEEK_AUTH_PATH || path.join(__dirname, '..', 'deepseek-auth.json'); - // wasmUrl в cURL обычно отсутствует — сохраняем рабочий из прошлого auth, иначе дефолт. - let wasmUrl = 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm'; - try { const old = JSON.parse(fs.readFileSync(dest, 'utf8')); if (old.wasmUrl) wasmUrl = old.wasmUrl; } catch { /* нет прошлого файла */ } - const wm = curl.match(/https?:\/\/[^\s'"]*sha3[^\s'"]*\.wasm/i); if (wm) wasmUrl = wm[0]; // вдруг есть в cURL - const out = { token, hif_dliq, hif_leim, cookie, wasmUrl }; - fs.writeFileSync(dest, JSON.stringify(out, null, 2)); - console.log('OK -> ' + dest); - console.log(' token: ' + token.length + ' символов'); - console.log(' cookie: ' + cookie.split(';').filter(Boolean).length + ' значений'); - console.log(' hif: ' + ((hif_dliq || hif_leim) ? 'захвачены' : 'нет (опционально)')); + const r = accounts.addAccount(parsed); + if (r.error) { console.error('Ошибка: ' + r.error); process.exit(2); } + console.log('OK: аккаунт добавлен в пул как ' + r.id + + ' (token ' + parsed.token.length + ' симв., cookie ' + parsed.cookie.split(';').filter(Boolean).length + ' знач.)'); })(); diff --git a/scripts/auth_from_har.js b/scripts/auth_from_har.js index d0bb83c..0ec3777 100644 --- a/scripts/auth_from_har.js +++ b/scripts/auth_from_har.js @@ -1,86 +1,30 @@ #!/usr/bin/env node /* - Импорт авторизации DeepSeek из HAR-файла. - HAR = DevTools -> Network -> правый клик -> "Save all as HAR" (или иконка экспорта). - Работает с любым браузером (Chrome/Firefox/Edge), где вы залогинены в DeepSeek. + Добавляет аккаунт DeepSeek из HAR-файла в ПУЛ (deepseek-accounts.json). + HAR = DevTools → Network → "Save all as HAR". Любой браузер. Использование: node scripts/auth_from_har.js "path/to/archive.har" - Скрипт сам выбирает лучший запрос к chat.deepseek.com/api/... с заголовком - Authorization: Bearer и извлекает token/cookie/hif/wasmUrl -> deepseek-auth.json. + Сам выбирает лучший запрос к chat.deepseek.com/api/... с Authorization: Bearer. */ const fs = require('fs'); -const path = require('path'); +const { parseHar, finalizeAuth } = require('../lib/parseAuth'); +const accounts = require('../accountManager'); const harPath = process.argv[2]; if (!harPath || !fs.existsSync(harPath)) { console.error('Укажите путь к .har: node scripts/auth_from_har.js "archive.har"'); process.exit(1); } -let har; -try { har = JSON.parse(fs.readFileSync(harPath, 'utf8')); } -catch (e) { console.error('Не удалось прочитать HAR (не JSON): ' + e.message); process.exit(1); } -const entries = (har.log && har.log.entries) || []; -const hv = (headers, name) => { - const h = (headers || []).find(x => (x.name || '').toLowerCase() === name); - return h ? (h.value || '') : ''; -}; - -// Выбираем лучший запрос: к deepseek, с Authorization: Bearer, по сумме полезных заголовков -let best = null; -for (const e of entries) { - const req = e.request || {}; - const url = req.url || ''; - if (!/deepseek\.com/i.test(url)) continue; - const auth = hv(req.headers, 'authorization'); - if (!/bearer\s+\S/i.test(auth)) continue; - const cookie = hv(req.headers, 'cookie'); - const dliq = hv(req.headers, 'x-hif-dliq'); - const leim = hv(req.headers, 'x-hif-leim'); - const score = (cookie ? 2 : 0) + (dliq ? 1 : 0) + (leim ? 1 : 0) + (/\/api\//.test(url) ? 1 : 0); - if (!best || score > best.score) best = { url, headers: req.headers, auth, cookie, dliq, leim, score }; -} - -if (!best) { - console.error('В HAR не найдено запросов к deepseek.com с заголовком Authorization: Bearer.'); - console.error('Сохраняйте HAR уже залогиненным и после отправки сообщения в DeepSeek.'); +const parsed = finalizeAuth(parseHar(fs.readFileSync(harPath, 'utf8')), accounts.anyWasmUrl()); +if (parsed.error) { + console.error('Ошибка: ' + parsed.error); + console.error('Сохраняйте HAR залогиненным и после отправки сообщения в DeepSeek.'); process.exit(2); } - -const token = best.auth.replace(/^Bearer\s+/i, '').trim(); -const cookie = best.cookie; -const hif_dliq = best.dliq; -const hif_leim = best.leim; - -// wasmUrl: ищем реальный запрос к sha3*.wasm в HAR, иначе дефолт -let wasmUrl = ''; -for (const e of entries) { - const u = (e.request && e.request.url) || ''; - if (/sha3.*\.wasm/i.test(u)) { wasmUrl = u; break; } -} -if (!wasmUrl) { - // не нашли в HAR — берём рабочий из прошлого auth, чтобы не подставлять устаревший дефолт - try { - const dp = process.env.DEEPSEEK_AUTH_PATH || path.join(__dirname, '..', 'deepseek-auth.json'); - const old = JSON.parse(fs.readFileSync(dp, 'utf8')); - if (old.wasmUrl) wasmUrl = old.wasmUrl; - } catch { /* нет прошлого файла */ } -} -if (!wasmUrl) wasmUrl = 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm'; - -if (!token || !cookie) { - console.error('Не хватает данных: ' + (!token ? 'token ' : '') + (!cookie ? 'cookie' : '')); - process.exit(2); -} - -const out = { token, hif_dliq, hif_leim, cookie, wasmUrl }; -const dest = process.env.DEEPSEEK_AUTH_PATH || path.join(__dirname, '..', 'deepseek-auth.json'); -fs.writeFileSync(dest, JSON.stringify(out, null, 2)); -console.log('OK -> ' + dest); -console.log(' source: ' + best.url.slice(0, 72)); -console.log(' token: ' + token.length + ' символов'); -console.log(' cookie: ' + cookie.split(';').filter(Boolean).length + ' значений'); -console.log(' hif: ' + ((hif_dliq || hif_leim) ? 'захвачены' : 'нет (опционально)')); -console.log(' wasmUrl: ' + (/^https/.test(wasmUrl) ? 'ok' : 'default')); +const r = accounts.addAccount(parsed); +if (r.error) { console.error('Ошибка: ' + r.error); process.exit(2); } +console.log('OK: аккаунт добавлен в пул как ' + r.id + + ' (token ' + parsed.token.length + ' симв., cookie ' + parsed.cookie.split(';').filter(Boolean).length + ' знач.)'); diff --git a/server.js b/server.js index 3180031..b3970b4 100755 --- a/server.js +++ b/server.js @@ -16,6 +16,8 @@ const os = require('os'); const path = require('path'); const readline = require('readline'); const { spawnSync } = require('child_process'); +const accounts = require('./accountManager'); +const { parseAuthInput, finalizeAuth } = require('./lib/parseAuth'); const SERVER_HOST = os.hostname(); // Dynamic hostname detection const SERVER_PUBLIC_IP = (() => { @@ -63,7 +65,8 @@ const SESSION_TTL_MS = 2 * 60 * 60 * 1000; // 2 hours const DS_CONFIG_PATH = process.env.DEEPSEEK_AUTH_PATH || path.join(__dirname, 'deepseek-auth.json'); let DS_CONFIG = {}; let BASE_HEADERS = {}; -function buildBaseHeaders() { +function buildBaseHeaders(account) { + const a = account || DS_CONFIG; // account задаётся при мультиаккаунтном вызове; fallback — legacy DS_CONFIG return { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36", "x-client-platform": "web", @@ -71,12 +74,12 @@ function buildBaseHeaders() { "x-client-locale": "ru", "x-client-timezone-offset": "14400", "x-app-version": "2.0.0", - "Authorization": `Bearer ${DS_CONFIG.token || ''}`, - "x-hif-dliq": DS_CONFIG.hif_dliq || '', - "x-hif-leim": DS_CONFIG.hif_leim || '', + "Authorization": `Bearer ${a.token || ''}`, + "x-hif-dliq": a.hif_dliq || '', + "x-hif-leim": a.hif_leim || '', "Origin": "https://chat.deepseek.com", "Referer": "https://chat.deepseek.com/", - "Cookie": DS_CONFIG.cookie || '', + "Cookie": a.cookie || '', "Content-Type": "application/json", }; } @@ -107,9 +110,33 @@ function createSession() { createdAt: null, messageCount: 0, history: [], + accountId: null, // под каким аккаунтом создан chat_session (для ротации) }; } +// Сброс серверной session (chat принадлежит конкретному аккаунту DeepSeek). +function resetSession(session) { + session.id = null; + session.parentMessageId = null; + session.createdAt = null; + session.messageCount = 0; +} + +// Статус аккаунта для дашборда: OK / WAIT (лимит) / INVALID / EXPIRED. +function accountStatus(a) { + const now = Date.now(); + if (a.invalid) return 'INVALID'; + const { exp } = accounts.decodeTokenInfo(a.token); + if (exp && exp <= now) return 'EXPIRED'; + if (a.resetAt && Date.parse(a.resetAt) > now) return 'WAIT'; + return 'OK'; +} +// Управление аккаунтами разрешаем только с локальной машины. +function isLocal(req) { + const ip = (req.socket && req.socket.remoteAddress) || ''; + return ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1'; +} + function getOrCreateAgentSession(agentId) { if (!sessions.has(agentId)) { sessions.set(agentId, createSession()); @@ -117,8 +144,8 @@ function getOrCreateAgentSession(agentId) { return sessions.get(agentId); } -async function solvePOW(challenge) { - const resp = await fetch(DS_CONFIG.wasmUrl); +async function solvePOW(challenge, wasmUrl) { + const resp = await fetch(wasmUrl || DS_CONFIG.wasmUrl); const wasmBytes = await resp.arrayBuffer(); const mod = await WebAssembly.instantiate(wasmBytes, { wbg: {} }); const e = mod.instance.exports; @@ -246,47 +273,43 @@ function resolveModelConfig(model) { function isKnownModel(model) { return Object.prototype.hasOwnProperty.call(MODEL_CONFIGS, String(model || '').toLowerCase()); } function isSupportedModel(model) { return resolveModelConfig(model).supported === true; } -async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default') { +async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default', account = null) { const modelCfg = resolveModelConfig(model); const session = getOrCreateAgentSession(agentId); - const agentTag = `[${agentId}]`; + const agentTag = `[${account ? account.id : agentId}]`; + const headers = buildBaseHeaders(account); // per-account заголовки (token/cookie/hif) + const wasmUrl = account ? account.wasmUrl : undefined; // Auto-reset on deep message chain if (session.id && session.messageCount >= MAX_MESSAGE_DEPTH) { console.log(`${agentTag} Session ${session.id} hit ${session.messageCount} messages. Auto-resetting.`); - session.id = null; - session.parentMessageId = null; - session.createdAt = null; - session.messageCount = 0; - // History preserved for context injection + resetSession(session); } // Reset expired sessions (DeepSeek web sessions last ~1-2 hours) if (session.id && session.createdAt && (Date.now() - session.createdAt > SESSION_TTL_MS)) { console.log(`${agentTag} Session ${session.id} expired (age: ${Math.round((Date.now() - session.createdAt) / 60000)}min). Creating new...`); - session.id = null; - session.parentMessageId = null; - session.createdAt = null; - session.messageCount = 0; + resetSession(session); } const cr = await fetch('https://chat.deepseek.com/api/v0/chat/create_pow_challenge', { - method: 'POST', headers: BASE_HEADERS, + method: 'POST', headers, body: JSON.stringify({ target_path: '/api/v0/chat/completion' }) }); const chalJson = JSON.parse(await cr.text()); const challenge = chalJson.data.biz_data.challenge; - const answer = await solvePOW(challenge); + const answer = await solvePOW(challenge, wasmUrl); if (!session.id) { const sr = await fetch('https://chat.deepseek.com/api/v0/chat_session/create', { - method: 'POST', headers: BASE_HEADERS, body: '{}' + method: 'POST', headers, body: '{}' }); const sessionData = await sr.json(); session.id = sessionData.data.biz_data.chat_session?.id || sessionData.data.biz_data.id; session.parentMessageId = null; session.createdAt = Date.now(); session.messageCount = 0; + if (account) session.accountId = account.id; console.log(`${agentTag} Created new session: ${session.id}`); } else { console.log(`${agentTag} Reusing session: ${session.id} (parent: ${session.parentMessageId}, msg#${session.messageCount})`); @@ -299,7 +322,7 @@ async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default') { })).toString('base64'); const resp = await fetch('https://chat.deepseek.com/api/v0/chat/completion', { method: 'POST', - headers: { ...BASE_HEADERS, 'X-DS-PoW-Response': powB64 }, + headers: { ...headers, 'X-DS-PoW-Response': powB64 }, body: JSON.stringify({ chat_session_id: session.id, parent_message_id: session.parentMessageId, @@ -310,24 +333,32 @@ async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default') { }) }); - // If session expired, reset and retry once if (resp.status !== 200) { const errText = await resp.text(); - console.log(`${agentTag} Session error (${resp.status}): ${errText.substring(0, 100)}`); + console.log(`${agentTag} completion error (${resp.status}): ${errText.substring(0, 120)}`); + // Лимит исчерпан → сигнал ротации на другой аккаунт + if (resp.status === 429) { + let hours; + const ra = (resp.headers && resp.headers.get) ? resp.headers.get('retry-after') : null; + if (ra && /^\d+$/.test(ra)) hours = Math.max(1, Math.round(Number(ra) / 3600)); + return { resp, agentId, account, rateLimited: true, retryAfterHours: hours }; + } + // Невалидный токен/cookie/PoW → пометить аккаунт невалидным + if (resp.status === 401 || resp.status === 403 || /"code"\s*:\s*(40003|40300|40301)/.test(errText)) { + return { resp, agentId, account, invalid: true }; + } + // Истёкшая сессия → пересоздать и повторить один раз (тот же аккаунт) if (resp.status === 400 || resp.status === 404 || resp.status === 500) { console.log(`${agentTag} Session ${session.id} expired. Creating new session...`); - session.id = null; - session.parentMessageId = null; - session.createdAt = null; - session.messageCount = 0; - + resetSession(session); const sr2 = await fetch('https://chat.deepseek.com/api/v0/chat_session/create', { - method: 'POST', headers: BASE_HEADERS, body: '{}' + method: 'POST', headers, body: '{}' }); const sessionData2 = await sr2.json(); session.id = sessionData2.data.biz_data.chat_session?.id || sessionData2.data.biz_data.id; session.parentMessageId = null; session.createdAt = Date.now(); + if (account) session.accountId = account.id; console.log(`${agentTag} Created new session: ${session.id}`); const newPowB64 = Buffer.from(JSON.stringify({ @@ -337,7 +368,7 @@ async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default') { })).toString('base64'); const resp2 = await fetch('https://chat.deepseek.com/api/v0/chat/completion', { method: 'POST', - headers: { ...BASE_HEADERS, 'X-DS-PoW-Response': newPowB64 }, + headers: { ...headers, 'X-DS-PoW-Response': newPowB64 }, body: JSON.stringify({ chat_session_id: session.id, parent_message_id: null, @@ -347,11 +378,42 @@ async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default') { action: null, preempt: false, }) }); - return { resp: resp2, agentId }; + return { resp: resp2, agentId, account }; } } - return { resp, agentId }; + return { resp, agentId, account }; +} + +// Сколько раз пробовать сменить аккаунт при лимите/инвалиде за один запрос. +const MAX_ACCOUNT_RETRIES = 6; + +// Обёртка с ротацией: берёт доступный аккаунт; при 429 помечает rate-limited, +// при 401/403 — invalid, и переходит к следующему. Возвращает { resp, account }. +async function askWithRotation(prompt, agentId, model) { + const session = getOrCreateAgentSession(agentId); + let last = 'none'; + for (let i = 0; i < MAX_ACCOUNT_RETRIES; i++) { + const account = accounts.getAvailableAccount(); + if (!account) return { resp: null, account: null, noAccounts: true }; + if (session.accountId && session.accountId !== account.id) resetSession(session); // chat принадлежал другому аккаунту + let r; + try { r = await askDeepSeekStream(prompt, agentId, model, account); } + catch (e) { console.log(`[${account.id}] ошибка запроса (${e.message}) — ротация на следующий`); accounts.markRateLimited(account.id, 1); resetSession(session); last = 'error'; continue; } + if (r.rateLimited) { + console.log(`[${account.id}] HTTP 429 — лимит, ротация на следующий аккаунт`); + accounts.markRateLimited(account.id, r.retryAfterHours); + resetSession(session); last = 'rate'; continue; + } + if (r.invalid) { + console.log(`[${account.id}] невалиден (401/403) — помечаю и ротация`); + accounts.markInvalid(account.id); + resetSession(session); last = 'invalid'; continue; + } + session.accountId = account.id; + return { resp: r.resp, account }; + } + return { resp: null, account: null, exhausted: last }; } // === Tool Calling Support === @@ -958,8 +1020,9 @@ const server = http.createServer(async (req, res) => { // Health check if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/health')) { + const _accs = accounts.listAccounts(); res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ status: 'ok', service: 'FreeDeepseekAPI', watermark: FORGETMEAI_WATERMARK, models: SUPPORTED_MODEL_IDS, unsupported_models: Object.keys(MODEL_CONFIGS).filter(id => !MODEL_CONFIGS[id].supported), agents: sessions.size, config_ready: hasAuthConfig() })); + res.end(JSON.stringify({ status: 'ok', service: 'FreeDeepseekAPI', watermark: FORGETMEAI_WATERMARK, models: SUPPORTED_MODEL_IDS, unsupported_models: Object.keys(MODEL_CONFIGS).filter(id => !MODEL_CONFIGS[id].supported), agents: sessions.size, config_ready: _accs.some(a => accountStatus(a) === 'OK'), accounts: { total: _accs.length, online: _accs.filter(a => accountStatus(a) === 'OK').length, limited: _accs.filter(a => accountStatus(a) === 'WAIT').length } })); return; } @@ -1036,22 +1099,81 @@ const server = http.createServer(async (req, res) => { // Auth status for dashboard: decode JWT exp (no signature check) + presence flags if (req.method === 'GET' && url.pathname === '/api/auth-status') { - let tokenExp = null; - try { - const payload = JSON.parse(Buffer.from(String(DS_CONFIG.token || '').split('.')[1], 'base64url').toString()); - if (payload.exp) tokenExp = payload.exp * 1000; - } catch { /* token missing or not a JWT */ } + const list = accounts.listAccounts(); + const ok = list.find(a => accountStatus(a) === 'OK'); + const tokenExp = ok ? accounts.decodeTokenInfo(ok.token).exp : null; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ - config_ready: hasAuthConfig(), + config_ready: accounts.hasValidAccounts(), + accounts_total: list.length, + accounts_online: list.filter(a => accountStatus(a) === 'OK').length, + accounts_limited: list.filter(a => accountStatus(a) === 'WAIT').length, token_exp: tokenExp, - has_token: !!DS_CONFIG.token, - has_cookie: !!DS_CONFIG.cookie, - has_hif: !!(DS_CONFIG.hif_dliq || DS_CONFIG.hif_leim), + has_token: list.length > 0, + has_cookie: list.length > 0, + has_hif: list.some(a => a.hif_dliq || a.hif_leim), })); return; } + // ── Управление аккаунтами DeepSeek (только localhost) ── + if (url.pathname === '/api/accounts' || url.pathname.startsWith('/api/accounts/')) { + if (!isLocal(req)) { res.writeHead(403, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Доступно только с localhost' })); return; } + + // GET /api/accounts — список со статусами + if (req.method === 'GET' && url.pathname === '/api/accounts') { + const list = accounts.listAccounts().map(a => ({ + id: a.id, status: accountStatus(a), + exp: accounts.decodeTokenInfo(a.token).exp, resetAt: a.resetAt || null, + preview: String(a.token || '').slice(-6), + })); + res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ accounts: list })); return; + } + + // POST /api/accounts/import — добавить аккаунт из cURL/HAR (тело запроса) + if (req.method === 'POST' && url.pathname === '/api/accounts/import') { + let body = ''; + req.on('data', c => { body += c; if (body.length > 25 * 1024 * 1024) req.destroy(); }); + req.on('end', () => { + try { + const parsed = finalizeAuth(parseAuthInput(body), accounts.anyWasmUrl()); + if (parsed.error) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(parsed)); return; } + const r = accounts.addAccount(parsed); + res.writeHead(r.error ? 400 : 200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(r)); + } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Ошибка импорта: ' + e.message })); } + }); + return; + } + + // POST /api/accounts/:id/check — реальная проверка аккаунта + const mCheck = url.pathname.match(/^\/api\/accounts\/(acc_[a-zA-Z0-9]+)\/check$/); + if (req.method === 'POST' && mCheck) { + const id = mCheck[1]; const acc = accounts.getAccountById(id); + if (!acc) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Аккаунт не найден' })); return; } + (async () => { + let status = 'ERROR'; + try { + const r = await askDeepSeekStream('ping', 'healthcheck-' + id, 'deepseek-chat', acc); + if (r.rateLimited) { accounts.markRateLimited(id, r.retryAfterHours); status = 'WAIT'; } + else if (r.invalid) { accounts.markInvalid(id); status = 'INVALID'; } + else if (r.resp && r.resp.status === 200) { accounts.markValid(id); status = 'OK'; } + try { if (r.resp && r.resp.body) r.resp.body.cancel(); } catch { /* noop */ } + } catch (e) { status = 'ERROR'; } + res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ id, status, exp: accounts.decodeTokenInfo(acc.token).exp })); + })(); + return; + } + + // DELETE /api/accounts/:id или POST /api/accounts/:id/delete + const mDel = url.pathname.match(/^\/api\/accounts\/(acc_[a-zA-Z0-9]+)(\/delete)?$/); + if (mDel && (req.method === 'DELETE' || (req.method === 'POST' && mDel[2]))) { + const r = accounts.deleteAccount(mDel[1]); + res.writeHead(r.error ? 400 : 200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(r)); return; + } + + res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Неизвестный эндпоинт аккаунтов' })); return; + } + const apiMode = url.pathname === '/v1/messages' ? 'anthropic' : (url.pathname === '/v1/responses' ? 'responses' : 'openai'); @@ -1107,7 +1229,13 @@ const server = http.createServer(async (req, res) => { : `${historyPrefix}${prompt}`; const startTime = Date.now(); - const { resp: dsResp } = await askDeepSeekStream(fullPrompt, agentId, requestedModel); + const { resp: dsResp, account: dsAccount, noAccounts } = await askWithRotation(fullPrompt, agentId, requestedModel); + if (noAccounts || !dsResp) { + res.writeHead(503, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: { message: 'Нет доступных аккаунтов DeepSeek (все в лимите или невалидны). Добавьте/обновите аккаунт в дашборде.', type: 'no_available_accounts' } })); + return; + } + let curAccount = dsAccount; // Process streaming response from DeepSeek — returns { content, reasoningContent, messageId, finishReason } async function readDeepSeekResponse(readable) { @@ -1244,7 +1372,15 @@ const server = http.createServer(async (req, res) => { session.messageCount = 0; // Brief delay before retry to let DeepSeek breathe await new Promise(r => setTimeout(r, Math.min(1000 * retryAttempt, 5000))); - const { resp: retryResp } = await askDeepSeekStream(fullPrompt, agentId, requestedModel); + // Пустой ответ часто = скрытый лимит: после пары попыток помечаем аккаунт и ротируем. + if (retryAttempt >= 3 && curAccount) { accounts.markRateLimited(curAccount.id); resetSession(session); } + const { resp: retryResp, account: retryAccount, noAccounts: noAcc2 } = await askWithRotation(fullPrompt, agentId, requestedModel); + if (noAcc2 || !retryResp) { + res.writeHead(503, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: { message: 'Все аккаунты DeepSeek в лимите или невалидны.', type: 'no_available_accounts' } })); + return; + } + if (retryAccount) curAccount = retryAccount; const retryResult = await readDeepSeekResponse(retryResp.body); const retryContent = retryResult && retryResult.content ? sanitizeContent(retryResult.content) : ''; const retryReasoning = retryResult && retryResult.reasoningContent ? sanitizeContent(retryResult.reasoningContent) : ''; @@ -1263,7 +1399,8 @@ const server = http.createServer(async (req, res) => { continuationRounds++; console.log(`${agentTag} Response ${fullContent.length} chars (finish=${finishReason}). Auto-continuing (${continuationRounds}/${MAX_CONTINUATION})...`); await new Promise(r => setTimeout(r, 500)); - const { resp: contResp } = await askDeepSeekStream('continue', agentId, requestedModel); + const { resp: contResp } = await askWithRotation('continue', agentId, requestedModel); + if (!contResp) break; const contResult = await readDeepSeekResponse(contResp.body); const contContent = contResult && contResult.content ? sanitizeContent(contResult.content) : ''; const contReasoning = contResult && contResult.reasoningContent ? sanitizeContent(contResult.reasoningContent) : ''; @@ -1289,8 +1426,8 @@ const server = http.createServer(async (req, res) => { session.messageCount = 0; await new Promise(r => setTimeout(r, 1000)); const strictPrompt = fullPrompt + '\n\n[STRICT INSTRUCTION] Your previous response had a TOOL_CALL but the arguments were too long and got cut off. Keep the arguments SHORT — no large file contents. Just use a minimal example or reference the file by name. Output ONLY: TOOL_CALL: \narguments: '; - const { resp: retryResp2 } = await askDeepSeekStream(strictPrompt, agentId, requestedModel); - const retryResult2 = await readDeepSeekResponse(retryResp2.body); + const { resp: retryResp2 } = await askWithRotation(strictPrompt, agentId, requestedModel); + const retryResult2 = retryResp2 ? await readDeepSeekResponse(retryResp2.body) : null; const retryContent2 = retryResult2 && retryResult2.content ? sanitizeContent(retryResult2.content) : ''; if (retryContent2 && retryContent2.trim()) { const retryTc = parseToolCall(retryContent2); From 49367cca37f1955c76dd40824b7f94077af4b1f4 Mon Sep 17 00:00:00 2001 From: dansc Date: Sun, 7 Jun 2026 18:28:21 +0300 Subject: [PATCH 06/10] feat(extension): one-click 'Add to FreeDeepseekAPI' + cross-browser (Firefox/Chrome) Extension popup gets a primary button that collects creds (token+httpOnly cookies+hif) and POSTs them to http://localhost:9655/api/accounts/import in one click. Manifest: browser_specific_settings.gecko for Firefox, localhost host_permissions, removed missing icon refs. server parseAuth now also accepts a ready JSON body (what the extension sends), not only cURL/HAR. Added extension README with Firefox/Chrome install steps. Co-Authored-By: Claude Opus 4.8 --- chrome-extension/README.md | 29 +++++++++++++++++++++++++++++ chrome-extension/manifest.json | 29 ++++++++++++++++++----------- chrome-extension/popup.html | 13 ++++++++----- chrome-extension/popup.js | 30 ++++++++++++++++++++++++++++++ lib/parseAuth.js | 7 +++++++ 5 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 chrome-extension/README.md diff --git a/chrome-extension/README.md b/chrome-extension/README.md new file mode 100644 index 0000000..3b58632 --- /dev/null +++ b/chrome-extension/README.md @@ -0,0 +1,29 @@ +# DeepSeek → FreeDeepseekAPI (расширение) + +Добавляет аккаунт DeepSeek в локальный FreeDeepseekAPI **одним кликом**: собирает +`token` + cookie (включая httpOnly) + `hif_*` с `chat.deepseek.com` и отправляет +на `http://localhost:9655/api/accounts/import`. + +Работает в Firefox и Chrome/Edge (Manifest V3). + +## Установка + +**Firefox** +1. Откройте `about:debugging#/runtime/this-firefox` +2. «Загрузить временное дополнение» → выберите `manifest.json` из этой папки. + (Временное дополнение: после перезапуска Firefox установить заново.) + +**Chrome / Edge** +1. Откройте `chrome://extensions` +2. Включите «Режим разработчика». +3. «Загрузить распакованное» → выберите эту папку. + +## Использование +1. Запустите FreeDeepseekAPI (порт 9655). +2. Откройте `chat.deepseek.com` и войдите в нужный аккаунт. +3. Клик по иконке расширения → **«➕ Добавить в FreeDeepseekAPI»**. + +Для нескольких аккаунтов повторите из разных профилей/логинов браузера. + +Вспомогательные кнопки: «Собрать» (показать креды), «Копировать JSON», +«Скачать файл» (`deepseek-auth.json`) — на случай ручного импорта через дашборд. diff --git a/chrome-extension/manifest.json b/chrome-extension/manifest.json index 0dd2fa3..20fad04 100644 --- a/chrome-extension/manifest.json +++ b/chrome-extension/manifest.json @@ -1,10 +1,22 @@ { "manifest_version": 3, - "name": "DeepSeek Auth Exporter", - "version": "1.0", - "description": "Extract DeepSeek Chat credentials from cookies + localStorage for use with the web API proxy", - "permissions": ["cookies", "storage", "downloads"], - "host_permissions": ["https://chat.deepseek.com/*"], + "name": "DeepSeek → FreeDeepseekAPI", + "version": "1.1", + "description": "Добавляет аккаунт DeepSeek в локальный FreeDeepseekAPI одним кликом (token + cookie + hif с chat.deepseek.com).", + "author": "FreeDeepseekAPI", + "browser_specific_settings": { + "gecko": { + "id": "freedeepseek-auth@forgetmeai", + "strict_min_version": "121.0", + "data_collection_permissions": { "required": ["none"] } + } + }, + "permissions": ["cookies", "storage"], + "host_permissions": [ + "https://chat.deepseek.com/*", + "http://localhost:9655/*", + "http://127.0.0.1:9655/*" + ], "content_scripts": [ { "matches": ["https://chat.deepseek.com/*"], @@ -17,11 +29,6 @@ }, "action": { "default_popup": "popup.html", - "default_icon": { - "48": "icon.png" - } - }, - "icons": { - "48": "icon.png" + "default_title": "DeepSeek → FreeDeepseekAPI" } } diff --git a/chrome-extension/popup.html b/chrome-extension/popup.html index f5837d7..d8b3d73 100644 --- a/chrome-extension/popup.html +++ b/chrome-extension/popup.html @@ -25,15 +25,18 @@ -

🔑 DeepSeek Auth Exporter

-
Export credentials for FreeDeepseekAPI
+

🔑 DeepSeek → FreeDeepseekAPI

+
Откройте chat.deepseek.com (залогинены) и нажмите кнопку
⏳ Loading...
- - - + +
+
+ + +
diff --git a/chrome-extension/popup.js b/chrome-extension/popup.js index 4e86805..be0fa67 100644 --- a/chrome-extension/popup.js +++ b/chrome-extension/popup.js @@ -76,6 +76,36 @@ $('btnCollect').addEventListener('click', () => { }); }); +const PROXY_URL = 'http://localhost:9655/api/accounts/import'; +// Главная кнопка: собрать креды и сразу отправить в локальный FreeDeepseekAPI +$('btnAdd').addEventListener('click', () => { + $('status').className = 'status warn'; + $('status').textContent = '⏳ Сбор и отправка в FreeDeepseekAPI…'; + chrome.runtime.sendMessage({ action: 'collect' }, async (response) => { + if (!response || !response.success) { + $('status').className = 'status err'; + $('status').textContent = '❌ ' + (response?.error || 'Откройте вкладку chat.deepseek.com'); + return; + } + render(response.auth); + const auth = buildAuthJson(response.auth); + if (!auth.token || !auth.cookie) { + $('status').className = 'status err'; + $('status').textContent = '❌ Не хватает token/cookie — войдите в DeepSeek и отправьте сообщение'; + return; + } + try { + const r = await fetch(PROXY_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(auth) }); + const j = await r.json(); + if (j.ok) { $('status').className = 'status ok'; $('status').textContent = '✅ Добавлен в FreeDeepseekAPI как ' + j.id; } + else { $('status').className = 'status err'; $('status').textContent = '❌ ' + (j.error || 'Ошибка добавления'); } + } catch (e) { + $('status').className = 'status err'; + $('status').textContent = '❌ FreeDeepseekAPI недоступен на localhost:9655 (запущен?)'; + } + }); +}); + // Copy JSON button $('btnCopy').addEventListener('click', () => { const json = $('jsonPreview').textContent; diff --git a/lib/parseAuth.js b/lib/parseAuth.js index 41320be..b333b9a 100644 --- a/lib/parseAuth.js +++ b/lib/parseAuth.js @@ -73,6 +73,13 @@ function parseAuthInput(text) { const s = String(text || '').trim(); if (!s) return { error: 'Пустой ввод' }; if (s[0] === '{' || s[0] === '[') { + // готовый JSON {token,cookie,...} (например, из расширения-экспортёра) + 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 ниже */ } const r = parseHar(s); if (!r.error) return r; // это был HAR } From 8c611b9f5608f794f6939adf4cb9503b54a0dfff Mon Sep 17 00:00:00 2001 From: dansc Date: Sun, 7 Jun 2026 18:37:53 +0300 Subject: [PATCH 07/10] fix(extension): Firefox background.scripts + switch to webRequest header capture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Firefox stable disables background.service_worker, so declare both service_worker (Chrome) and scripts (Firefox). The cookie-based collection missed token (it's in the Authorization header, not a cookie), some cookies, and hif (x-hif-* request headers). Switched to webRequest.onBeforeSendHeaders on chat.deepseek.com/api/* to capture exactly what HAR/cURL would тАФ token + full cookie + hif. UX: send one message in DeepSeek, then click Add. Removed content.js (no longer needed). Co-Authored-By: Claude Opus 4.8 --- chrome-extension/README.md | 10 +- chrome-extension/background.js | 138 +++++++++------------------- chrome-extension/content.js | 16 ---- chrome-extension/manifest.json | 16 +--- chrome-extension/popup.html | 2 +- chrome-extension/popup.js | 163 ++++++++++----------------------- 6 files changed, 105 insertions(+), 240 deletions(-) delete mode 100644 chrome-extension/content.js diff --git a/chrome-extension/README.md b/chrome-extension/README.md index 3b58632..169369d 100644 --- a/chrome-extension/README.md +++ b/chrome-extension/README.md @@ -1,8 +1,9 @@ # DeepSeek → FreeDeepseekAPI (расширение) -Добавляет аккаунт DeepSeek в локальный FreeDeepseekAPI **одним кликом**: собирает -`token` + cookie (включая httpOnly) + `hif_*` с `chat.deepseek.com` и отправляет -на `http://localhost:9655/api/accounts/import`. +Добавляет аккаунт DeepSeek в локальный FreeDeepseekAPI **одним кликом**: +перехватывает заголовки реального запроса к `chat.deepseek.com/api/...` +(`token` из `Authorization`, все cookie, `hif_*`) и отправляет на +`http://localhost:9655/api/accounts/import`. Работает в Firefox и Chrome/Edge (Manifest V3). @@ -21,7 +22,8 @@ ## Использование 1. Запустите FreeDeepseekAPI (порт 9655). 2. Откройте `chat.deepseek.com` и войдите в нужный аккаунт. -3. Клик по иконке расширения → **«➕ Добавить в FreeDeepseekAPI»**. +3. **Отправьте любое сообщение** (например `ok`) — чтобы прошёл запрос, из которого берутся креды. +4. Клик по иконке расширения → **«➕ Добавить в FreeDeepseekAPI»**. Для нескольких аккаунтов повторите из разных профилей/логинов браузера. diff --git a/chrome-extension/background.js b/chrome-extension/background.js index 7b32d94..481c500 100644 --- a/chrome-extension/background.js +++ b/chrome-extension/background.js @@ -1,97 +1,45 @@ -// DeepSeek Auth Exporter — Background Service Worker -// Reads cookies from Chrome, forwards content-script localStorage data. - -const STORAGE_KEY = 'deepseek_auth'; - -// Read all needed cookies from chat.deepseek.com -async function readCookies() { - const needed = ['token', 'ds_session_id', 'smidV2']; - const results = {}; - for (const name of needed) { - const cookie = await new Promise((resolve) => - chrome.cookies.get({ url: 'https://chat.deepseek.com', name }, resolve) - ); - results[name] = cookie ? cookie.value : ''; - } - - // Build cookie header string - const parts = []; - if (results.ds_session_id) parts.push(`ds_session_id=${results.ds_session_id}`); - if (results.smidV2) parts.push(`smidV2=${results.smidV2}`); - results.cookie = parts.join('; '); - - return results; -} - -// Read localStorage values via content script injection -async function readLocalStorage(tabId) { - const keys = ['hif_dliq', 'hif_leim']; - try { - const results = await new Promise((resolve, reject) => { - chrome.tabs.sendMessage( - tabId, - { action: 'readLocalStorage', keys }, - (response) => { - if (chrome.runtime.lastError) reject(chrome.runtime.lastError.message); - else resolve(response.data || {}); +// DeepSeek → FreeDeepseekAPI — перехват заголовков реального запроса. +// token (Authorization: Bearer), cookie (все), hif (x-hif-*) берутся из +// настоящего запроса к chat.deepseek.com/api/... — как в 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 даёт их без него. +const opts = ['requestHeaders']; +try { + if (chrome.webRequest.OnBeforeSendHeadersOptions && + chrome.webRequest.OnBeforeSendHeadersOptions.EXTRA_HEADERS) { + opts.push('extraHeaders'); + } +} catch (e) { /* Firefox: опции нет — это нормально */ } + +chrome.webRequest.onBeforeSendHeaders.addListener( + (details) => { + const h = {}; + for (const x of (details.requestHeaders || [])) h[x.name.toLowerCase()] = x.value; + const auth = h['authorization'] || ''; + const token = /^bearer\s+\S/i.test(auth) ? auth.replace(/^bearer\s+/i, '').trim() : ''; + const cookie = h['cookie'] || ''; + if (token && cookie) { + const cap = { + token, + cookie, + hif_dliq: h['x-hif-dliq'] || '', + hif_leim: h['x-hif-leim'] || '', + wasmUrl: WASM_DEFAULT, + _t: Date.now(), + }; + chrome.storage.local.set({ [KEY]: cap }); } - ); - }); - return results; - } catch (e) { - return {}; - } -} - -// Find an open DeepSeek tab -function findDeepSeekTab() { - return new Promise((resolve) => { - chrome.tabs.query( - { url: 'https://chat.deepseek.com/*' }, - (tabs) => resolve(tabs.length > 0 ? tabs[0] : null) - ); - }); -} - -async function collectAndStore(tabId) { - const cookies = await readCookies(); - let ls = {}; - if (tabId) ls = await readLocalStorage(tabId); - - const merged = { - token: cookies.token || '', - ds_session_id: cookies.ds_session_id || '', - smidV2: cookies.smidV2 || '', - cookie: cookies.cookie || '', - hif_dliq: ls.hif_dliq || '', - hif_leim: ls.hif_leim || '', - _lastUpdated: new Date().toISOString(), - }; - - await new Promise((resolve) => - chrome.storage.local.set({ [STORAGE_KEY]: merged }, resolve) - ); - return merged; -} - -// Message handler — popup requests -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.action === 'collect') { - findDeepSeekTab().then(async (tab) => { - if (!tab) { - sendResponse({ success: false, error: 'No DeepSeek tab open' }); - return; - } - const auth = await collectAndStore(tab.id); - sendResponse({ success: true, auth }); - }); - return true; // keep channel open for async - } - - if (request.action === 'export') { - chrome.storage.local.get(STORAGE_KEY, (result) => { - sendResponse({ success: true, auth: result[STORAGE_KEY] || {} }); - }); - return true; - } + }, + { urls: ['https://chat.deepseek.com/api/*'] }, + opts +); + +chrome.runtime.onMessage.addListener((req, sender, sendResponse) => { + if (req.action === 'get') { + chrome.storage.local.get(KEY, (r) => sendResponse({ success: true, cap: r[KEY] || null })); + return true; // async + } }); diff --git a/chrome-extension/content.js b/chrome-extension/content.js deleted file mode 100644 index ad04f8c..0000000 --- a/chrome-extension/content.js +++ /dev/null @@ -1,16 +0,0 @@ -// DeepSeek Auth Exporter — Content Script -// Runs on chat.deepseek.com to read localStorage values. - -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.action === 'readLocalStorage') { - const data = {}; - for (const key of request.keys) { - try { - data[key] = localStorage.getItem(key) || ''; - } catch (e) { - data[key] = ''; - } - } - sendResponse({ data }); - } -}); diff --git a/chrome-extension/manifest.json b/chrome-extension/manifest.json index 20fad04..930fdf5 100644 --- a/chrome-extension/manifest.json +++ b/chrome-extension/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, "name": "DeepSeek → FreeDeepseekAPI", - "version": "1.1", - "description": "Добавляет аккаунт DeepSeek в локальный FreeDeepseekAPI одним кликом (token + cookie + hif с chat.deepseek.com).", + "version": "1.2", + "description": "Добавляет аккаунт DeepSeek в локальный FreeDeepseekAPI одним кликом. Перехватывает заголовки запроса chat.deepseek.com (token + cookie + hif).", "author": "FreeDeepseekAPI", "browser_specific_settings": { "gecko": { @@ -11,21 +11,15 @@ "data_collection_permissions": { "required": ["none"] } } }, - "permissions": ["cookies", "storage"], + "permissions": ["webRequest", "storage"], "host_permissions": [ "https://chat.deepseek.com/*", "http://localhost:9655/*", "http://127.0.0.1:9655/*" ], - "content_scripts": [ - { - "matches": ["https://chat.deepseek.com/*"], - "js": ["content.js"], - "run_at": "document_idle" - } - ], "background": { - "service_worker": "background.js" + "service_worker": "background.js", + "scripts": ["background.js"] }, "action": { "default_popup": "popup.html", diff --git a/chrome-extension/popup.html b/chrome-extension/popup.html index d8b3d73..9bd69db 100644 --- a/chrome-extension/popup.html +++ b/chrome-extension/popup.html @@ -26,7 +26,7 @@

🔑 DeepSeek → FreeDeepseekAPI

-
Откройте chat.deepseek.com (залогинены) и нажмите кнопку
+
Откройте chat.deepseek.com, отправьте любое сообщение, затем нажмите кнопку
⏳ Loading...
diff --git a/chrome-extension/popup.js b/chrome-extension/popup.js index be0fa67..12873bc 100644 --- a/chrome-extension/popup.js +++ b/chrome-extension/popup.js @@ -1,133 +1,70 @@ -// DeepSeek Auth Exporter — Popup Script - +// DeepSeek → FreeDeepseekAPI — Popup function $(id) { return document.getElementById(id); } +const PROXY_URL = 'http://localhost:9655/api/accounts/import'; -const WASM_URL = 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm'; - -function buildAuthJson(data) { - const cookie = []; - if (data.ds_session_id) cookie.push(`ds_session_id=${data.ds_session_id}`); - if (data.smidV2) cookie.push(`smidV2=${data.smidV2}`); - - return { - token: data.token || '', - hif_dliq: data.hif_dliq || '', - hif_leim: data.hif_leim || '', - cookie: cookie.join('; '), - wasmUrl: WASM_URL, - }; -} - -function getStatus(auth, data) { - const checks = [ - { label: 'token', ok: !!auth.token }, - { label: 'cookie (ds_session_id / smidV2)', ok: auth.cookie.includes('=') }, - { label: 'hif_dliq', ok: !!auth.hif_dliq }, - { label: 'hif_leim', ok: !!auth.hif_leim }, - ]; - return { checks, allOk: checks.every((c) => c.ok) }; -} - -function render(data) { - const auth = buildAuthJson(data); - const preview = JSON.stringify(auth, null, 2); - $('jsonPreview').textContent = preview; - - const { checks, allOk } = getStatus(auth, data); - const missing = checks.filter((c) => !c.ok).map((c) => c.label); +let current = null; // перехваченный набор кредов {token,cookie,hif_*,wasmUrl} - if (!data._lastUpdated) { - $('status').className = 'status warn'; - $('status').textContent = '⚠️ No credentials yet. Click "Collect from Tab" while on chat.deepseek.com'; - } else if (allOk) { +function render(cap) { + if (!cap || !cap.token || !cap.cookie) { + $('status').className = 'status warn'; + $('status').textContent = '⚠️ Откройте chat.deepseek.com и ОТПРАВЬТЕ любое сообщение, затем нажмите кнопку.'; + $('jsonPreview').textContent = '{ }'; + $('detail').textContent = 'Креды появятся после запроса к DeepSeek'; + return null; + } + const auth = { token: cap.token, hif_dliq: cap.hif_dliq || '', hif_leim: cap.hif_leim || '', cookie: cap.cookie, wasmUrl: cap.wasmUrl }; + // превью с маскировкой секретов + $('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); $('status').className = 'status ok'; - $('status').textContent = '✅ All 4 credentials captured — ready to export'; - } else { - $('status').className = 'status warn'; - $('status').textContent = `⚠️ Missing: ${missing.join(', ')}`; - } - - $('detail').textContent = data._lastUpdated - ? `Last updated: ${data._lastUpdated}` - : 'Open chat.deepseek.com, then click Collect'; + $('status').textContent = '✅ Перехвачено: token + cookie' + (auth.hif_leim ? ' + hif' : '') + ' — готово'; + $('detail').textContent = cap._t ? ('Обновлено: ' + new Date(cap._t).toLocaleTimeString()) : ''; + return auth; } -function loadAuth() { - chrome.runtime.sendMessage({ action: 'export' }, (response) => { - if (response && response.success) render(response.auth); - else { - $('status').className = 'status err'; - $('status').textContent = '❌ Failed to read stored credentials'; - } - }); +function refresh() { + chrome.runtime.sendMessage({ action: 'get' }, (r) => { current = (r && r.success) ? render(r.cap) : render(null); }); } -// Collect button — reads cookies + localStorage from active DeepSeek tab -$('btnCollect').addEventListener('click', () => { - $('status').className = 'status warn'; - $('status').textContent = '⏳ Collecting from chat.deepseek.com...'; - chrome.runtime.sendMessage({ action: 'collect' }, (response) => { - if (response && response.success) { - render(response.auth); - } else { - $('status').className = 'status err'; - $('status').textContent = '❌ ' + (response?.error || 'Unknown error'); - } - }); -}); - -const PROXY_URL = 'http://localhost:9655/api/accounts/import'; -// Главная кнопка: собрать креды и сразу отправить в локальный FreeDeepseekAPI -$('btnAdd').addEventListener('click', () => { - $('status').className = 'status warn'; - $('status').textContent = '⏳ Сбор и отправка в FreeDeepseekAPI…'; - chrome.runtime.sendMessage({ action: 'collect' }, async (response) => { - if (!response || !response.success) { - $('status').className = 'status err'; - $('status').textContent = '❌ ' + (response?.error || 'Откройте вкладку chat.deepseek.com'); - return; - } - render(response.auth); - const auth = buildAuthJson(response.auth); - if (!auth.token || !auth.cookie) { - $('status').className = 'status err'; - $('status').textContent = '❌ Не хватает token/cookie — войдите в DeepSeek и отправьте сообщение'; - return; +// Главная кнопка — отправить перехваченные креды в FreeDeepseekAPI +$('btnAdd').addEventListener('click', async () => { + if (!current) { + refresh(); + $('status').className = 'status warn'; + $('status').textContent = '⏳ Кредов нет. Отправьте сообщение в DeepSeek и нажмите снова.'; + return; } + $('status').className = 'status warn'; + $('status').textContent = '⏳ Отправка в FreeDeepseekAPI…'; try { - const r = await fetch(PROXY_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(auth) }); - const j = await r.json(); - if (j.ok) { $('status').className = 'status ok'; $('status').textContent = '✅ Добавлен в FreeDeepseekAPI как ' + j.id; } - else { $('status').className = 'status err'; $('status').textContent = '❌ ' + (j.error || 'Ошибка добавления'); } + 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) { $('status').className = 'status ok'; $('status').textContent = '✅ Добавлен в FreeDeepseekAPI как ' + j.id; } + else { $('status').className = 'status err'; $('status').textContent = '❌ ' + (j.error || 'Ошибка добавления'); } } catch (e) { - $('status').className = 'status err'; - $('status').textContent = '❌ FreeDeepseekAPI недоступен на localhost:9655 (запущен?)'; + $('status').className = 'status err'; + $('status').textContent = '❌ FreeDeepseekAPI недоступен на localhost:9655 (запущен?)'; } - }); }); -// Copy JSON button +$('btnCollect').addEventListener('click', refresh); + $('btnCopy').addEventListener('click', () => { - const json = $('jsonPreview').textContent; - navigator.clipboard.writeText(json).then(() => { - $('btnCopy').textContent = '✅ Copied!'; - setTimeout(() => { $('btnCopy').textContent = '📋 Copy JSON'; }, 1500); - }); + if (!current) return; + navigator.clipboard.writeText(JSON.stringify(current, null, 2)).then(() => { + $('btnCopy').textContent = '✅'; setTimeout(() => { $('btnCopy').textContent = '📋 Копировать JSON'; }, 1200); + }); }); -// Download file button $('btnSave').addEventListener('click', () => { - const json = $('jsonPreview').textContent; - const blob = new Blob([json + '\n'], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'deepseek-auth.json'; - a.click(); - URL.revokeObjectURL(url); - $('btnSave').textContent = '✅ Saved!'; - setTimeout(() => { $('btnSave').textContent = '💾 Download File'; }, 1500); + if (!current) return; + const blob = new Blob([JSON.stringify(current, null, 2) + '\n'], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); a.href = url; a.download = 'deepseek-auth.json'; a.click(); + URL.revokeObjectURL(url); }); -// Initial load -loadAuth(); +refresh(); From a444dd55a505e8a75caa5bf6b6bdc61526beb9d7 Mon Sep 17 00:00:00 2001 From: dansc Date: Sun, 7 Jun 2026 21:07:51 +0300 Subject: [PATCH 08/10] fix(server): classify dead DeepSeek tokens as invalid, not rate-limit DeepSeek returns HTTP 200 + {code:40003,data:null} for an invalid/expired token (not 401), so the old code crashed on data.biz_data and askWithRotation caught it as a generic error -> markRateLimited, masking dead creds as a temporary cap. Added parseBizData() + DeepSeekAuthError so auth-error codes (40003/40300/40301) mark the account invalid while other failures stay a temporary rate-limit. Co-Authored-By: Claude Opus 4.8 --- server.js | 51 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/server.js b/server.js index b3970b4..791b80d 100755 --- a/server.js +++ b/server.js @@ -273,6 +273,35 @@ function resolveModelConfig(model) { function isKnownModel(model) { return Object.prototype.hasOwnProperty.call(MODEL_CONFIGS, String(model || '').toLowerCase()); } function isSupportedModel(model) { return resolveModelConfig(model).supported === true; } +// DeepSeek отвечает HTTP 200 с code!=0 даже при ошибке авторизации, например +// {code:40003,msg:"Authorization Failed (invalid token)",data:null} +// Такие коды означают невалидные креды (а не временный лимит), поэтому аккаунт +// нужно метить invalid, а не rate-limited. Без этой проверки обращение к +// data.biz_data падало с "Cannot read properties of null", и обёртка ошибочно +// трактовала это как rate-limit. +const DS_AUTH_ERROR_CODES = new Set([40003, 40300, 40301]); +class DeepSeekAuthError extends Error { + constructor(code, msg) { + super(`DeepSeek авторизация отклонена (code ${code}${msg ? ': ' + msg : ''})`); + this.name = 'DeepSeekAuthError'; + this.code = code; + this.isAuthError = true; + } +} +// Разбирает ответ DeepSeek и возвращает data.biz_data. Бросает DeepSeekAuthError +// при коде авторизации и обычную ошибку при пустом/неразобранном теле — чтобы +// askWithRotation мог отличить invalid от временного сбоя. +function parseBizData(rawText, what) { + let j; + try { j = JSON.parse(rawText); } + catch { throw new Error(`Не удалось разобрать ответ DeepSeek (${what})`); } + if (j && DS_AUTH_ERROR_CODES.has(j.code)) throw new DeepSeekAuthError(j.code, j.msg); + if (!j || !j.data || !j.data.biz_data) { + throw new Error(`Пустой data.biz_data в ответе DeepSeek (${what}, code=${j ? j.code : 'n/a'})`); + } + return j.data.biz_data; +} + async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default', account = null) { const modelCfg = resolveModelConfig(model); const session = getOrCreateAgentSession(agentId); @@ -296,16 +325,15 @@ async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default', ac method: 'POST', headers, body: JSON.stringify({ target_path: '/api/v0/chat/completion' }) }); - const chalJson = JSON.parse(await cr.text()); - const challenge = chalJson.data.biz_data.challenge; + const challenge = parseBizData(await cr.text(), 'create_pow_challenge').challenge; const answer = await solvePOW(challenge, wasmUrl); if (!session.id) { const sr = await fetch('https://chat.deepseek.com/api/v0/chat_session/create', { method: 'POST', headers, body: '{}' }); - const sessionData = await sr.json(); - session.id = sessionData.data.biz_data.chat_session?.id || sessionData.data.biz_data.id; + const sessionBiz = parseBizData(await sr.text(), 'chat_session/create'); + session.id = sessionBiz.chat_session?.id || sessionBiz.id; session.parentMessageId = null; session.createdAt = Date.now(); session.messageCount = 0; @@ -354,8 +382,8 @@ async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default', ac const sr2 = await fetch('https://chat.deepseek.com/api/v0/chat_session/create', { method: 'POST', headers, body: '{}' }); - const sessionData2 = await sr2.json(); - session.id = sessionData2.data.biz_data.chat_session?.id || sessionData2.data.biz_data.id; + const sessionBiz2 = parseBizData(await sr2.text(), 'chat_session/create(retry)'); + session.id = sessionBiz2.chat_session?.id || sessionBiz2.id; session.parentMessageId = null; session.createdAt = Date.now(); if (account) session.accountId = account.id; @@ -399,7 +427,16 @@ async function askWithRotation(prompt, agentId, model) { if (session.accountId && session.accountId !== account.id) resetSession(session); // chat принадлежал другому аккаунту let r; try { r = await askDeepSeekStream(prompt, agentId, model, account); } - catch (e) { console.log(`[${account.id}] ошибка запроса (${e.message}) — ротация на следующий`); accounts.markRateLimited(account.id, 1); resetSession(session); last = 'error'; continue; } + catch (e) { + if (e && e.isAuthError) { + console.log(`[${account.id}] невалиден (${e.message}) — помечаю invalid и ротация`); + accounts.markInvalid(account.id); last = 'invalid'; + } else { + console.log(`[${account.id}] ошибка запроса (${e.message}) — временная, ротация`); + accounts.markRateLimited(account.id, 1); last = 'error'; + } + resetSession(session); continue; + } if (r.rateLimited) { console.log(`[${account.id}] HTTP 429 — лимит, ротация на следующий аккаунт`); accounts.markRateLimited(account.id, r.retryAfterHours); From 9d2e861ec5d588d93956b862a6a2806fa87ccc9a Mon Sep 17 00:00:00 2001 From: dansc Date: Sun, 7 Jun 2026 21:57:27 +0300 Subject: [PATCH 09/10] feat(extension): account pool panel with live status, email, auto-validation Popup now shows the account pool (GET /api/accounts) with per-account status badges (OK/INVALID/WAIT) and check/delete actions, auto-validates each account right after import via /check, shows the account email, and warns on duplicate imports instead of a bare error. Pool UI is built with DOM APIs (no innerHTML) to avoid XSS. Backend: server resolves email via users/current on import and lazily backfills it on /check; addAccount stores email and returns existingId on duplicate; GET /api/accounts exposes email; accountManager gains setEmail. parseAuth.js and background.js unchanged. Co-Authored-By: Claude Opus 4.8 --- accountManager.js | 10 ++- chrome-extension/manifest.json | 2 +- chrome-extension/popup.html | 30 ++++++++ chrome-extension/popup.js | 129 +++++++++++++++++++++++++++++---- server.js | 23 ++++-- 5 files changed, 170 insertions(+), 24 deletions(-) diff --git a/accountManager.js b/accountManager.js index 18e62a0..aa183df 100644 --- a/accountManager.js +++ b/accountManager.js @@ -90,23 +90,25 @@ function markRateLimited(id, hours) { } function markInvalid(id) { return _update(id, a => { a.invalid = true; }); } function markValid(id) { return _update(id, a => { a.invalid = false; a.resetAt = null; }); } +function setEmail(id, email) { return _update(id, a => { a.email = String(email || ''); }); } function addAccount(obj) { if (!obj || !obj.token) return { error: 'Нужен token' }; if (!obj.cookie) return { error: 'Нужен cookie' }; const a = loadAccounts(); - if (a.some(x => x.token === obj.token)) return { error: 'Этот аккаунт уже добавлен' }; + const dup = a.find(x => x.token === obj.token); + if (dup) return { error: 'Этот аккаунт уже добавлен', existingId: dup.id }; let n = 1; const ids = new Set(a.map(x => x.id)); while (ids.has('acc_' + n)) n++; const id = 'acc_' + n; a.push({ id, token: obj.token, cookie: obj.cookie, hif_dliq: obj.hif_dliq || '', hif_leim: obj.hif_leim || '', - wasmUrl: obj.wasmUrl || '', resetAt: null, invalid: false, + wasmUrl: obj.wasmUrl || '', email: obj.email || '', resetAt: null, invalid: false, }); saveAccounts(a); const { exp } = decodeTokenInfo(obj.token); - return { ok: true, id, exp }; + return { ok: true, id, exp, email: obj.email || '' }; } function deleteAccount(id) { @@ -124,5 +126,5 @@ function anyWasmUrl() { const a = loadAccounts().find(x => x.wasmUrl); return a module.exports = { loadAccounts, saveAccounts, listAccounts, getAvailableAccount, getAccountById, hasValidAccounts, hasAnyAccount, markRateLimited, markInvalid, markValid, - addAccount, deleteAccount, decodeTokenInfo, anyWasmUrl, COOLDOWN_HOURS, + addAccount, deleteAccount, setEmail, decodeTokenInfo, anyWasmUrl, COOLDOWN_HOURS, }; diff --git a/chrome-extension/manifest.json b/chrome-extension/manifest.json index 930fdf5..5ff23de 100644 --- a/chrome-extension/manifest.json +++ b/chrome-extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "DeepSeek → FreeDeepseekAPI", - "version": "1.2", + "version": "1.3", "description": "Добавляет аккаунт DeepSeek в локальный FreeDeepseekAPI одним кликом. Перехватывает заголовки запроса chat.deepseek.com (token + cookie + hif).", "author": "FreeDeepseekAPI", "browser_specific_settings": { diff --git a/chrome-extension/popup.html b/chrome-extension/popup.html index 9bd69db..b72ad1d 100644 --- a/chrome-extension/popup.html +++ b/chrome-extension/popup.html @@ -22,6 +22,28 @@ .json-display { background: #0d1117; border: 1px solid #333; border-radius: 4px; padding: 8px; font-size: 10px; font-family: 'Courier New', monospace; color: #7ee787; white-space: pre-wrap; word-break: break-all; max-height: 180px; overflow-y: auto; margin-bottom: 8px; } .field label { display: block; font-size: 11px; color: #999; margin-bottom: 4px; } .detail { font-size: 10px; color: #666; text-align: center; } + .pool-section { margin-top: 14px; border-top: 1px solid #333; padding-top: 10px; } + .pool-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } + .pool-header .title { font-size: 12px; color: #4fc3f7; font-weight: 600; } + .link-btn { background: none; border: 1px solid #444; color: #9ccc65; border-radius: 5px; padding: 4px 8px; font-size: 11px; cursor: pointer; } + .link-btn:hover { background: #ffffff10; } + .link-btn:disabled { opacity: 0.5; cursor: default; } + .pool-list { display: flex; flex-direction: column; gap: 4px; max-height: 220px; overflow-y: auto; } + .pool-row { display: flex; align-items: center; gap: 6px; padding: 6px 8px; background: #0d1117; border: 1px solid #2a2a3a; border-radius: 5px; font-size: 11px; } + .pool-row .id { color: #888; font-family: 'Courier New', monospace; min-width: 40px; } + .pool-row .email { flex: 1; color: #ccc; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .badge { padding: 2px 7px; border-radius: 10px; font-size: 10px; font-weight: 600; white-space: nowrap; } + .badge.ok { background: #1b5e2033; color: #66bb6a; border: 1px solid #2e7d32; } + .badge.invalid { background: #b71c1c33; color: #ef5350; border: 1px solid #c62828; } + .badge.wait { background: #e6510033; color: #ff9800; border: 1px solid #e65100; } + .badge.expired { background: #44444433; color: #999; border: 1px solid #555; } + .badge.checking { background: #1565c033; color: #4fc3f7; border: 1px solid #1565c0; } + .row-actions { display: flex; gap: 4px; } + .acc-btn { background: #ffffff0d; border: 1px solid #444; color: #ddd; border-radius: 4px; padding: 3px 7px; font-size: 11px; cursor: pointer; } + .acc-btn:hover { background: #ffffff1a; } + .acc-btn.danger:hover { background: #b71c1c44; color: #ef5350; } + .acc-btn.confirm { background: #b71c1c; color: #fff; border-color: #c62828; } + .pool-empty { color: #666; font-size: 11px; text-align: center; padding: 10px; } @@ -46,6 +68,14 @@

🔑 DeepSeek → FreeDeepseekAPI

Open chat.deepseek.com, then click Collect
+
+
+ Пул аккаунтов + +
+
+
+ diff --git a/chrome-extension/popup.js b/chrome-extension/popup.js index 12873bc..e2c4127 100644 --- a/chrome-extension/popup.js +++ b/chrome-extension/popup.js @@ -1,13 +1,15 @@ // DeepSeek → FreeDeepseekAPI — Popup function $(id) { return document.getElementById(id); } -const PROXY_URL = 'http://localhost:9655/api/accounts/import'; +const API_BASE = 'http://localhost:9655'; +const PROXY_URL = API_BASE + '/api/accounts/import'; let current = null; // перехваченный набор кредов {token,cookie,hif_*,wasmUrl} +function setStatus(cls, text) { $('status').className = 'status ' + cls; $('status').textContent = text; } + function render(cap) { if (!cap || !cap.token || !cap.cookie) { - $('status').className = 'status warn'; - $('status').textContent = '⚠️ Откройте chat.deepseek.com и ОТПРАВЬТЕ любое сообщение, затем нажмите кнопку.'; + setStatus('warn', '⚠️ Откройте chat.deepseek.com и ОТПРАВЬТЕ любое сообщение, затем нажмите кнопку.'); $('jsonPreview').textContent = '{ }'; $('detail').textContent = 'Креды появятся после запроса к DeepSeek'; return null; @@ -19,8 +21,7 @@ function render(cap) { cookie: auth.cookie.slice(0, 48) + '…', hif_leim: auth.hif_leim ? ('…(' + auth.hif_leim.length + ')') : '', }, null, 2); - $('status').className = 'status ok'; - $('status').textContent = '✅ Перехвачено: token + cookie' + (auth.hif_leim ? ' + hif' : '') + ' — готово'; + setStatus('ok', '✅ Перехвачено: token + cookie' + (auth.hif_leim ? ' + hif' : '') + ' — готово'); $('detail').textContent = cap._t ? ('Обновлено: ' + new Date(cap._t).toLocaleTimeString()) : ''; return auth; } @@ -29,24 +30,123 @@ function refresh() { chrome.runtime.sendMessage({ action: 'get' }, (r) => { current = (r && r.success) ? render(r.cap) : render(null); }); } -// Главная кнопка — отправить перехваченные креды в FreeDeepseekAPI +// ── Панель пула (строится через DOM API, без innerHTML, чтобы исключить 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; + return b; +} + +function setEmpty(text) { + const pool = $('pool'); pool.textContent = ''; + const e = document.createElement('div'); e.className = 'pool-empty'; e.textContent = text; + pool.appendChild(e); +} + +function renderPool(list) { + $('poolTitle').textContent = `Пул аккаунтов (${list.length})`; + if (!list.length) { setEmpty('Нет аккаунтов'); return; } + const pool = $('pool'); pool.textContent = ''; + for (const a of list) { + const row = document.createElement('div'); + row.className = 'pool-row'; row.dataset.id = a.id; + + const idEl = document.createElement('span'); + idEl.className = 'id'; idEl.textContent = a.id; + + const emailEl = document.createElement('span'); + emailEl.className = 'email'; emailEl.textContent = a.email || '—'; emailEl.title = a.email || ''; + + const badge = document.createElement('span'); + badge.className = 'badge ' + (a.status || '').toLowerCase(); badge.textContent = a.status || '—'; + + const actions = document.createElement('span'); + actions.className = 'row-actions'; + actions.append(mkBtn('check', '↻', 'Проверить', 'acc-btn'), mkBtn('del', '✕', 'Удалить', 'acc-btn danger')); + + row.append(idEl, emailEl, badge, actions); + pool.appendChild(row); + } +} + +async function loadPool() { + try { + const r = await fetch(API_BASE + '/api/accounts'); + const j = await r.json(); + renderPool(j.accounts || []); + } catch { + $('poolTitle').textContent = 'Пул аккаунтов'; + setEmpty('FreeDeepseekAPI недоступен на :9655'); + } +} + +async function checkAccount(id) { + const row = document.querySelector(`.pool-row[data-id="${id}"]`); + const b = row && row.querySelector('.badge'); + if (b) { b.className = 'badge checking'; b.textContent = '…'; } + try { + const r = await fetch(`${API_BASE}/api/accounts/${id}/check`, { method: 'POST' }); + const j = await r.json(); + if (b) { b.className = 'badge ' + (j.status || '').toLowerCase(); b.textContent = j.status || '—'; } + if (j.email && row) { const em = row.querySelector('.email'); em.textContent = j.email; em.title = j.email; } + return j.status; + } catch { if (b) { b.className = 'badge invalid'; b.textContent = 'ERR'; } return 'ERROR'; } +} + +async function deleteAccount(id) { + try { await fetch(`${API_BASE}/api/accounts/${id}`, { method: 'DELETE' }); } catch { /* noop */ } + loadPool(); +} + +// делегирование кликов в панели (check / удаление с инлайн-подтверждением) +$('pool').addEventListener('click', (e) => { + const btn = e.target.closest('.acc-btn'); if (!btn) return; + const row = btn.closest('.pool-row'); const id = row && row.dataset.id; if (!id) return; + const act = btn.dataset.act; + 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')); + return; + } + if (act === 'yes') deleteAccount(id); + else if (act === 'no') loadPool(); +}); + +async function checkAll() { + $('btnCheckAll').disabled = true; + const ids = [...document.querySelectorAll('.pool-row')].map(r => r.dataset.id); + for (const id of ids) await checkAccount(id); + $('btnCheckAll').disabled = false; +} +$('btnCheckAll').addEventListener('click', checkAll); + +// ── Главная кнопка — добавить перехваченные креды + авто-валидация ── $('btnAdd').addEventListener('click', async () => { if (!current) { refresh(); - $('status').className = 'status warn'; - $('status').textContent = '⏳ Кредов нет. Отправьте сообщение в DeepSeek и нажмите снова.'; + setStatus('warn', '⏳ Кредов нет. Отправьте сообщение в DeepSeek и нажмите снова.'); return; } - $('status').className = 'status warn'; - $('status').textContent = '⏳ Отправка в FreeDeepseekAPI…'; + setStatus('warn', '⏳ Отправка в 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) { $('status').className = 'status ok'; $('status').textContent = '✅ Добавлен в FreeDeepseekAPI как ' + j.id; } - else { $('status').className = 'status err'; $('status').textContent = '❌ ' + (j.error || 'Ошибка добавления'); } + if (j.ok) { + const who = j.email ? ` (${j.email})` : ''; + setStatus('ok', `✅ Добавлен как ${j.id}${who} — проверяю…`); + await loadPool(); + const st = await checkAccount(j.id); + if (st === 'OK') setStatus('ok', `🟢 ${j.id}${who} — рабочий`); + else setStatus('err', `🔴 ${j.id} — статус: ${st || 'неизвестен'}`); + } else if (j.existingId) { + setStatus('warn', `⚠️ Уже добавлен как ${j.existingId}`); + loadPool(); + } else { + setStatus('err', '❌ ' + (j.error || 'Ошибка добавления')); + } } catch (e) { - $('status').className = 'status err'; - $('status').textContent = '❌ FreeDeepseekAPI недоступен на localhost:9655 (запущен?)'; + setStatus('err', '❌ FreeDeepseekAPI недоступен на localhost:9655 (запущен?)'); } }); @@ -68,3 +168,4 @@ $('btnSave').addEventListener('click', () => { }); refresh(); +loadPool(); diff --git a/server.js b/server.js index 791b80d..18b4351 100755 --- a/server.js +++ b/server.js @@ -83,6 +83,15 @@ function buildBaseHeaders(account) { "Content-Type": "application/json", }; } +// Получить email аккаунта по перехваченным кредам (GET users/current). +// Возвращает '' при любой ошибке/отсутствии — добавление аккаунта не блокируется. +async function fetchAccountEmail(creds) { + try { + const r = await fetch('https://chat.deepseek.com/api/v0/users/current', { headers: buildBaseHeaders(creds) }); + const j = await r.json(); + return (j && j.data && j.data.biz_data && j.data.biz_data.email) || ''; + } catch { return ''; } +} function loadDeepSeekConfig({ fatal = true } = {}) { try { const raw = fs.readFileSync(DS_CONFIG_PATH, 'utf8'); @@ -1160,7 +1169,7 @@ const server = http.createServer(async (req, res) => { // GET /api/accounts — список со статусами if (req.method === 'GET' && url.pathname === '/api/accounts') { const list = accounts.listAccounts().map(a => ({ - id: a.id, status: accountStatus(a), + id: a.id, status: accountStatus(a), email: a.email || '', exp: accounts.decodeTokenInfo(a.token).exp, resetAt: a.resetAt || null, preview: String(a.token || '').slice(-6), })); @@ -1171,12 +1180,14 @@ const server = http.createServer(async (req, res) => { if (req.method === 'POST' && url.pathname === '/api/accounts/import') { let body = ''; req.on('data', c => { body += c; if (body.length > 25 * 1024 * 1024) req.destroy(); }); - req.on('end', () => { + req.on('end', async () => { try { const parsed = finalizeAuth(parseAuthInput(body), accounts.anyWasmUrl()); if (parsed.error) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(parsed)); return; } - const r = accounts.addAccount(parsed); - res.writeHead(r.error ? 400 : 200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(r)); + const email = await fetchAccountEmail(parsed); + const r = accounts.addAccount({ ...parsed, email }); + const code = r.error ? (r.existingId ? 409 : 400) : 200; + res.writeHead(code, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(r)); } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Ошибка импорта: ' + e.message })); } }); return; @@ -1195,8 +1206,10 @@ const server = http.createServer(async (req, res) => { else if (r.invalid) { accounts.markInvalid(id); status = 'INVALID'; } else if (r.resp && r.resp.status === 200) { accounts.markValid(id); status = 'OK'; } try { if (r.resp && r.resp.body) r.resp.body.cancel(); } catch { /* noop */ } + if (status === 'OK' && !acc.email) { const em = await fetchAccountEmail(acc); if (em) accounts.setEmail(id, em); } } catch (e) { status = 'ERROR'; } - res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ id, status, exp: accounts.decodeTokenInfo(acc.token).exp })); + const fresh = accounts.getAccountById(id); + res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ id, status, email: (fresh && fresh.email) || '', exp: accounts.decodeTokenInfo(acc.token).exp })); })(); return; } From 2e4d4aae8e5afe60a92074e21c5c36551a3290c5 Mon Sep 17 00:00:00 2001 From: dansc Date: Mon, 8 Jun 2026 08:48:39 +0300 Subject: [PATCH 10/10] fix(security): block cross-origin requests to /api/accounts (CSRF) The account-management block was guarded only by an IP check (isLocal), which does not stop CSRF: a malicious page can cross-origin POST to http://localhost:9655/api/accounts/import with a simple content-type (no preflight) and inject attacker-controlled credentials into the victim's pool (or delete/relabel accounts). Add isCrossOrigin(): reject /api/accounts/* whose Origin/Referer host doesn't match the server host. Non-browser clients (no Origin/Referer) are allowed, so CLI/scripts and the same-origin dashboard keep working. Co-Authored-By: Claude Opus 4.8 --- server.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server.js b/server.js index 18b4351..7c4cac6 100755 --- a/server.js +++ b/server.js @@ -146,6 +146,16 @@ function isLocal(req) { return ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1'; } +// CSRF-защита для управления аккаунтами: блокируем межсайтовые запросы. +// Браузер всегда шлёт Origin (и Referer) при cross-origin запросе — отклоняем, +// если источник не совпадает с хостом сервера. Не-браузерные клиенты (curl/скрипты) +// не шлют ни Origin, ни Referer и CSRF-вектором не являются, поэтому пропускаются. +function isCrossOrigin(req) { + const src = req.headers.origin || req.headers.referer; + if (!src) return false; + try { return new URL(src).host !== req.headers.host; } catch { return true; } +} + function getOrCreateAgentSession(agentId) { if (!sessions.has(agentId)) { sessions.set(agentId, createSession()); @@ -1165,6 +1175,7 @@ const server = http.createServer(async (req, res) => { // ── Управление аккаунтами DeepSeek (только localhost) ── if (url.pathname === '/api/accounts' || url.pathname.startsWith('/api/accounts/')) { if (!isLocal(req)) { res.writeHead(403, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Доступно только с localhost' })); return; } + if (isCrossOrigin(req)) { res.writeHead(403, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Cross-origin запрос отклонён' })); return; } // GET /api/accounts — список со статусами if (req.method === 'GET' && url.pathname === '/api/accounts') {