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..aa183df --- /dev/null +++ b/accountManager.js @@ -0,0 +1,130 @@ +'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 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(); + 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 || '', email: obj.email || '', resetAt: null, invalid: false, + }); + saveAccounts(a); + const { exp } = decodeTokenInfo(obj.token); + return { ok: true, id, exp, email: obj.email || '' }; +} + +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, setEmail, decodeTokenInfo, anyWasmUrl, COOLDOWN_HOURS, +}; diff --git a/chrome-extension/README.md b/chrome-extension/README.md new file mode 100644 index 0000000..169369d --- /dev/null +++ b/chrome-extension/README.md @@ -0,0 +1,31 @@ +# DeepSeek → FreeDeepseekAPI (расширение) + +Добавляет аккаунт DeepSeek в локальный FreeDeepseekAPI **одним кликом**: +перехватывает заголовки реального запроса к `chat.deepseek.com/api/...` +(`token` из `Authorization`, все cookie, `hif_*`) и отправляет на +`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. **Отправьте любое сообщение** (например `ok`) — чтобы прошёл запрос, из которого берутся креды. +4. Клик по иконке расширения → **«➕ Добавить в FreeDeepseekAPI»**. + +Для нескольких аккаунтов повторите из разных профилей/логинов браузера. + +Вспомогательные кнопки: «Собрать» (показать креды), «Копировать JSON», +«Скачать файл» (`deepseek-auth.json`) — на случай ручного импорта через дашборд. 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 0dd2fa3..5ff23de 100644 --- a/chrome-extension/manifest.json +++ b/chrome-extension/manifest.json @@ -1,27 +1,28 @@ { "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/*"], - "content_scripts": [ - { - "matches": ["https://chat.deepseek.com/*"], - "js": ["content.js"], - "run_at": "document_idle" + "name": "DeepSeek → FreeDeepseekAPI", + "version": "1.3", + "description": "Добавляет аккаунт DeepSeek в локальный FreeDeepseekAPI одним кликом. Перехватывает заголовки запроса chat.deepseek.com (token + cookie + hif).", + "author": "FreeDeepseekAPI", + "browser_specific_settings": { + "gecko": { + "id": "freedeepseek-auth@forgetmeai", + "strict_min_version": "121.0", + "data_collection_permissions": { "required": ["none"] } } + }, + "permissions": ["webRequest", "storage"], + "host_permissions": [ + "https://chat.deepseek.com/*", + "http://localhost:9655/*", + "http://127.0.0.1:9655/*" ], "background": { - "service_worker": "background.js" + "service_worker": "background.js", + "scripts": ["background.js"] }, "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..b72ad1d 100644 --- a/chrome-extension/popup.html +++ b/chrome-extension/popup.html @@ -22,18 +22,43 @@ .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; } -

🔑 DeepSeek Auth Exporter

-
Export credentials for FreeDeepseekAPI
+

🔑 DeepSeek → FreeDeepseekAPI

+
Откройте chat.deepseek.com, отправьте любое сообщение, затем нажмите кнопку
⏳ Loading...
- - - + +
+
+ + +
@@ -43,6 +68,14 @@

🔑 DeepSeek Auth Exporter

Open chat.deepseek.com, then click Collect
+
+
+ Пул аккаунтов + +
+
+
+ diff --git a/chrome-extension/popup.js b/chrome-extension/popup.js index 4e86805..e2c4127 100644 --- a/chrome-extension/popup.js +++ b/chrome-extension/popup.js @@ -1,103 +1,171 @@ -// DeepSeek Auth Exporter — Popup Script - +// DeepSeek → FreeDeepseekAPI — Popup function $(id) { return document.getElementById(id); } +const API_BASE = 'http://localhost:9655'; +const PROXY_URL = API_BASE + '/api/accounts/import'; + +let current = null; // перехваченный набор кредов {token,cookie,hif_*,wasmUrl} -const WASM_URL = 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm'; +function setStatus(cls, text) { $('status').className = 'status ' + cls; $('status').textContent = text; } -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}`); +function render(cap) { + if (!cap || !cap.token || !cap.cookie) { + setStatus('warn', '⚠️ Откройте 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); + setStatus('ok', '✅ Перехвачено: token + cookie' + (auth.hif_leim ? ' + hif' : '') + ' — готово'); + $('detail').textContent = cap._t ? ('Обновлено: ' + new Date(cap._t).toLocaleTimeString()) : ''; + return auth; +} - return { - token: data.token || '', - hif_dliq: data.hif_dliq || '', - hif_leim: data.hif_leim || '', - cookie: cookie.join('; '), - wasmUrl: WASM_URL, - }; +function refresh() { + chrome.runtime.sendMessage({ action: 'get' }, (r) => { current = (r && r.success) ? render(r.cap) : render(null); }); } -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) }; +// ── Панель пула (строится через 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 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); - - if (!data._lastUpdated) { - $('status').className = 'status warn'; - $('status').textContent = '⚠️ No credentials yet. Click "Collect from Tab" while on chat.deepseek.com'; - } else if (allOk) { - $('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'; +function setEmpty(text) { + const pool = $('pool'); pool.textContent = ''; + const e = document.createElement('div'); e.className = 'pool-empty'; e.textContent = text; + pool.appendChild(e); } -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 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); } - }); } -// 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'); +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(); }); -// Copy JSON button +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(); + setStatus('warn', '⏳ Кредов нет. Отправьте сообщение в DeepSeek и нажмите снова.'); + return; + } + 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) { + 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) { + setStatus('err', '❌ FreeDeepseekAPI недоступен на localhost:9655 (запущен?)'); + } +}); + +$('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(); +loadPool(); diff --git a/lib/parseAuth.js b/lib/parseAuth.js new file mode 100644 index 0000000..b333b9a --- /dev/null +++ b/lib/parseAuth.js @@ -0,0 +1,107 @@ +'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] === '[') { + // готовый 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 + } + 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 new file mode 100644 index 0000000..8725ae3 --- /dev/null +++ b/public/dashboard.html @@ -0,0 +1,468 @@ + + + + + +FreeDeepseekAPI — Дашборд + + + + + + + + +
+
+

FreeDeepseekAPI

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

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

+
+
+ + +
+
+
+ + +
+
+

Аккаунты DeepSeek +

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

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

+
+
любой непустой (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/scripts/auth_from_curl.js b/scripts/auth_from_curl.js new file mode 100644 index 0000000..a1b2952 --- /dev/null +++ b/scripts/auth_from_curl.js @@ -0,0 +1,44 @@ +#!/usr/bin/env node +/* + Добавляет аккаунт DeepSeek из "Copy as cURL" в ПУЛ (deepseek-accounts.json). + Работает с любым браузером, где вы залогинены в chat.deepseek.com. + + Использование: + 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: chat.deepseek.com → F12 → Network → отправьте сообщение → + правый клик на запросе к /api/v0/... → Copy → Copy as cURL. +*/ +const fs = require('fs'); +const { parseAuthInput, finalizeAuth } = require('../lib/parseAuth'); +const accounts = require('../accountManager'); + +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)); + if (process.stdin.isTTY) resolve(''); + }); +} + +(async () => { + 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 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 new file mode 100644 index 0000000..0ec3777 --- /dev/null +++ b/scripts/auth_from_har.js @@ -0,0 +1,30 @@ +#!/usr/bin/env node +/* + Добавляет аккаунт 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. +*/ +const fs = require('fs'); +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); +} + +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 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 190f740..7c4cac6 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,15 +74,24 @@ 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", }; } +// Получить 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'); @@ -107,9 +119,43 @@ 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'; +} + +// 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()); @@ -117,8 +163,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 +292,71 @@ 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') { +// 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); - 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 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: 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; + 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; + 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 +369,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 +380,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; + 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; console.log(`${agentTag} Created new session: ${session.id}`); const newPowB64 = Buffer.from(JSON.stringify({ @@ -337,7 +415,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 +425,51 @@ 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) { + 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); + 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 +1076,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; } @@ -1021,6 +1140,101 @@ 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') { + 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: 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: 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; } + 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') { + const list = accounts.listAccounts().map(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), + })); + 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', 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 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; + } + + // 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 */ } + if (status === 'OK' && !acc.email) { const em = await fetchAccountEmail(acc); if (em) accounts.setEmail(id, em); } + } catch (e) { status = 'ERROR'; } + 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; + } + + // 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'); @@ -1076,7 +1290,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) { @@ -1213,7 +1433,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) : ''; @@ -1232,7 +1460,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) : ''; @@ -1258,8 +1487,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);