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; }
-FreeDeepseekAPIchat.deepseek.com, отправьте любое сообщение, затем нажмите кнопку