From fad61a65e53f81bc060c4bacd7eca675d988258e Mon Sep 17 00:00:00 2001 From: dansc Date: Tue, 9 Jun 2026 18:06:22 +0300 Subject: [PATCH 1/2] feat: web dashboard + chrome extension for runtime account management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a localhost-only web dashboard and a Chrome extension to manage the DeepSeek account pool at runtime, layered on top of the existing file-based pool (no duplicate pool): - routes: GET /dashboard, GET /api/accounts, POST /api/accounts/import (cURL/HAR), POST /api/accounts/:id/check, DELETE /api/accounts/:id, GET /api/auth-status — localhost-only and CSRF-guarded - runtime add/delete writes/removes managed files in data/accounts/ then reloads the pool; secrets written 0600 and gitignored - live account check via GET /api/v0/users/current (no PoW), 15s timeout - scripts/auth_from_curl.js + auth_from_har.js import helpers - lib/parseAuth.js parses cURL/HAR auth captures --- .gitignore | 2 + chrome-extension/README.md | 31 +++ chrome-extension/background.js | 138 +++------- chrome-extension/manifest.json | 35 +-- chrome-extension/popup.html | 43 ++- chrome-extension/popup.js | 230 ++++++++++------ data/accounts/.gitkeep | 0 lib/parseAuth.js | 107 ++++++++ public/dashboard.html | 468 +++++++++++++++++++++++++++++++++ scripts/auth_from_curl.js | 55 ++++ scripts/auth_from_har.js | 41 +++ server.js | 257 +++++++++++++++++- 12 files changed, 1204 insertions(+), 203 deletions(-) create mode 100644 chrome-extension/README.md create mode 100644 data/accounts/.gitkeep create mode 100644 lib/parseAuth.js create mode 100644 public/dashboard.html create mode 100644 scripts/auth_from_curl.js create mode 100644 scripts/auth_from_har.js diff --git a/.gitignore b/.gitignore index 2610c6d..a8cab52 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ node_modules/ *.log auth.json deepseek-auth.json +# Runtime-added auth files contain secrets — keep the dir, ignore its contents. +data/accounts/*.json .chrome-profile-deepseek/ .chrome-for-testing-profile-deepseek/ .chrome-for-testing-profile-deepseek.stale-*/ 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/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/data/accounts/.gitkeep b/data/accounts/.gitkeep new file mode 100644 index 0000000..e69de29 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..062eaec --- /dev/null +++ b/scripts/auth_from_curl.js @@ -0,0 +1,55 @@ +#!/usr/bin/env node +/* + Adds a DeepSeek account from "Copy as cURL" to the file-based pool by writing a + new auth file into the managed dir (data/accounts/account_.json). The server + picks it up on its next reload. Works with any browser logged in to chat.deepseek.com. + + Usage: + 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 # from clipboard (PowerShell) + + How to get cURL: chat.deepseek.com → F12 → Network → send a message → + right-click the /api/v0/... request → Copy → Copy as cURL. +*/ +const fs = require('fs'); +const path = require('path'); +const { parseAuthInput, finalizeAuth, WASM_DEFAULT } = require('../lib/parseAuth'); + +const MANAGED_AUTH_DIR = path.join(__dirname, '..', 'data', 'accounts'); + +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('Empty: pass cURL via stdin, a file argument, or the clipboard.'); process.exit(1); } + + const parsed = finalizeAuth(parseAuthInput(input), WASM_DEFAULT); + if (parsed.error) { + console.error('Error: ' + parsed.error); + console.error('Copy exactly the chat.deepseek.com/api/... request via "Copy as cURL".'); + process.exit(2); + } + const content = { + token: parsed.token, + cookie: parsed.cookie, + wasmUrl: parsed.wasmUrl || WASM_DEFAULT, + hif_dliq: parsed.hif_dliq || '', + hif_leim: parsed.hif_leim || '', + }; + fs.mkdirSync(MANAGED_AUTH_DIR, { recursive: true }); + const file = path.join(MANAGED_AUTH_DIR, `account_${Date.now()}.json`); + fs.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 0o600 }); + console.log('OK: account written to ' + file + + ' (token ' + parsed.token.length + ' chars, cookie ' + parsed.cookie.split(';').filter(Boolean).length + ' values)'); +})(); diff --git a/scripts/auth_from_har.js b/scripts/auth_from_har.js new file mode 100644 index 0000000..a9ac706 --- /dev/null +++ b/scripts/auth_from_har.js @@ -0,0 +1,41 @@ +#!/usr/bin/env node +/* + Adds a DeepSeek account from a HAR file to the file-based pool by writing a new + auth file into the managed dir (data/accounts/account_.json). The server picks + it up on its next reload. HAR = DevTools → Network → "Save all as HAR". Any browser. + + Usage: + node scripts/auth_from_har.js "path/to/archive.har" + + Picks the best chat.deepseek.com/api/... request with Authorization: Bearer. +*/ +const fs = require('fs'); +const path = require('path'); +const { parseHar, finalizeAuth, WASM_DEFAULT } = require('../lib/parseAuth'); + +const MANAGED_AUTH_DIR = path.join(__dirname, '..', 'data', 'accounts'); + +const harPath = process.argv[2]; +if (!harPath || !fs.existsSync(harPath)) { + console.error('Provide a path to a .har: node scripts/auth_from_har.js "archive.har"'); + process.exit(1); +} + +const parsed = finalizeAuth(parseHar(fs.readFileSync(harPath, 'utf8')), WASM_DEFAULT); +if (parsed.error) { + console.error('Error: ' + parsed.error); + console.error('Save the HAR while logged in and after sending a message in DeepSeek.'); + process.exit(2); +} +const content = { + token: parsed.token, + cookie: parsed.cookie, + wasmUrl: parsed.wasmUrl || WASM_DEFAULT, + hif_dliq: parsed.hif_dliq || '', + hif_leim: parsed.hif_leim || '', +}; +fs.mkdirSync(MANAGED_AUTH_DIR, { recursive: true }); +const file = path.join(MANAGED_AUTH_DIR, `account_${Date.now()}.json`); +fs.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 0o600 }); +console.log('OK: account written to ' + file + + ' (token ' + parsed.token.length + ' chars, cookie ' + parsed.cookie.split(';').filter(Boolean).length + ' values)'); diff --git a/server.js b/server.js index 3ba72a0..220a262 100755 --- a/server.js +++ b/server.js @@ -16,6 +16,7 @@ const os = require('os'); const path = require('path'); const readline = require('readline'); const { spawnSync } = require('child_process'); +const { parseAuthInput, finalizeAuth } = require('./lib/parseAuth'); const SERVER_HOST = os.hostname(); // Dynamic hostname detection const SERVER_PUBLIC_IP = (() => { @@ -61,7 +62,13 @@ const SESSION_TTL_MS = 2 * 60 * 60 * 1000; // 2 hours // === DeepSeek Web API Config — loaded from external config file === const DS_CONFIG_PATH = process.env.DEEPSEEK_AUTH_PATH || path.join(__dirname, 'deepseek-auth.json'); +// Managed runtime auth dir: accounts added through the dashboard / import scripts +// are written here as account_.json and picked up on the next reload. This +// directory is always scanned in addition to the env-provided paths, so runtime +// CRUD works on top of the maintainer's file-based pool without a separate store. +const MANAGED_AUTH_DIR = path.join(__dirname, 'data', 'accounts'); const DEFAULT_ACCOUNT_COOLDOWN_MS = Number(process.env.DEEPSEEK_ACCOUNT_COOLDOWN_MS || 10 * 60 * 1000); +const DEFAULT_WASM = 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm'; let DS_CONFIG = {}; let dsHeaders = {}; const accounts = []; @@ -83,22 +90,45 @@ function buildBaseHeaders(config = DS_CONFIG) { "Content-Type": "application/json", }; } +// *.json files from the managed runtime dir (data/accounts). Created on demand so +// runtime-added accounts survive reloads. Returns [] if the dir cannot be created. +function managedAuthPaths() { + try { + fs.mkdirSync(MANAGED_AUTH_DIR, { recursive: true }); + return fs.readdirSync(MANAGED_AUTH_DIR) + .filter(f => f.endsWith('.json')) + .sort() + .map(f => path.join(MANAGED_AUTH_DIR, f)); + } catch (e) { + console.error(`[DS-API] Could not read managed auth dir ${MANAGED_AUTH_DIR}: ${e.message}`); + return []; + } +} function discoverAuthPaths() { + let envPaths; if (process.env.DEEPSEEK_AUTH_DIR) { try { - return fs.readdirSync(process.env.DEEPSEEK_AUTH_DIR) + envPaths = fs.readdirSync(process.env.DEEPSEEK_AUTH_DIR) .filter(f => f.endsWith('.json')) .sort() .map(f => path.join(process.env.DEEPSEEK_AUTH_DIR, f)); } catch (e) { console.error(`[DS-API] Could not read DEEPSEEK_AUTH_DIR: ${e.message}`); - return []; + envPaths = []; } + } else if (process.env.DEEPSEEK_AUTH_PATH && process.env.DEEPSEEK_AUTH_PATH.includes(',')) { + envPaths = process.env.DEEPSEEK_AUTH_PATH.split(',').map(s => s.trim()).filter(Boolean); + } else { + envPaths = [DS_CONFIG_PATH]; } - if (process.env.DEEPSEEK_AUTH_PATH && process.env.DEEPSEEK_AUTH_PATH.includes(',')) { - return process.env.DEEPSEEK_AUTH_PATH.split(',').map(s => s.trim()).filter(Boolean); + // Always also include the managed runtime dir, de-duplicated against env paths + // (resolved to absolute) so the same file is never loaded twice. + const seen = new Set(envPaths.map(p => path.resolve(p))); + const merged = [...envPaths]; + for (const p of managedAuthPaths()) { + if (!seen.has(path.resolve(p))) { seen.add(path.resolve(p)); merged.push(p); } } - return [DS_CONFIG_PATH]; + return merged; } function loadDeepSeekConfig({ fatal = true } = {}) { accounts.length = 0; @@ -174,6 +204,118 @@ function markAccountFailure(account, status, reason = '') { console.log(`[account:${account.id}] cooldown for ${Math.round(DEFAULT_ACCOUNT_COOLDOWN_MS / 1000)}s after HTTP ${status}${reason ? ` (${reason})` : ''}`); } } + +// === Account management (dashboard / import) on top of the file-based pool === + +// Decode a JWT payload's `exp` (in ms) without verifying the signature. +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 }; } +} + +// String status used by the dashboard. Distinct from accountStatus() (object form) +// which is consumed by /health and the rest of the server. +function accountStatusStr(account) { + if (!account.config.token || !account.config.cookie) return 'INVALID'; + const { exp } = decodeTokenInfo(account.config.token); + if (exp && exp <= Date.now()) return 'EXPIRED'; + if (account.cooldownUntil > Date.now()) return 'WAIT'; + return 'OK'; +} + +// True only when the given absolute path lives inside the managed runtime dir. +// Guards file deletion so env-provided auth files are never touched. +function isManagedFile(file) { + if (!file) return false; + const dir = path.resolve(MANAGED_AUTH_DIR); + const rel = path.relative(dir, path.resolve(file)); + return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel); +} + +// Add an account from a parsed auth object ({token,cookie,wasmUrl,hif_dliq,hif_leim,email}). +// Dedups by token; writes a new file into the managed dir; reloads the pool. +function addAccountFromAuth(parsed) { + const token = parsed && parsed.token; + if (!token) return { error: 'Need token' }; + if (!parsed.cookie) return { error: 'Need cookie' }; + const dup = accounts.find(a => a.config.token === token); + if (dup) return { error: 'This account is already added', existingId: dup.id }; + const content = { + token, + cookie: parsed.cookie, + wasmUrl: parsed.wasmUrl || '', + hif_dliq: parsed.hif_dliq || '', + hif_leim: parsed.hif_leim || '', + email: parsed.email || '', + }; + try { + fs.mkdirSync(MANAGED_AUTH_DIR, { recursive: true }); + const file = path.join(MANAGED_AUTH_DIR, `account_${Date.now()}.json`); + fs.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 0o600 }); + } catch (e) { + return { error: 'Could not save account: ' + e.message }; + } + loadDeepSeekConfig({ fatal: false }); + const added = accounts.find(a => a.config.token === token); + return { ok: true, id: added ? added.id : null }; +} + +// Delete an account by id. Only removes the backing file when it is inside the +// managed dir; env-provided files are left in place. Reloads the pool. +function deleteAccountById(id) { + const account = accounts.find(a => a.id === id); + if (!account) return { error: 'Account not found' }; + if (account.file && isManagedFile(account.file)) { + try { fs.unlinkSync(account.file); } + catch (e) { return { error: 'Could not delete account file: ' + e.message }; } + } + loadDeepSeekConfig({ fatal: false }); + return { ok: true }; +} + +// Public, dashboard-friendly view of the pool. +function listAccountsPublic() { + return accounts.map(a => ({ + id: a.id, + status: accountStatusStr(a), + email: a.config.email || '', + exp: decodeTokenInfo(a.config.token).exp, + resetAt: a.cooldownUntil > Date.now() ? new Date(a.cooldownUntil).toISOString() : null, + preview: String(a.config.token || '').slice(-6), + })); +} + +// Live liveness check via GET users/current — validates token/cookie directly, +// needs no PoW (PoW is only required for completion). Hard 15s timeout so the +// /check route can never hang. Returns { status, email }; never throws. +async function checkAccountLive(account) { + try { + const resp = await fetch('https://chat.deepseek.com/api/v0/users/current', { + headers: buildBaseHeaders(account.config), + signal: AbortSignal.timeout(15000), + }); + const text = await resp.text(); + let j = null; + try { j = text ? JSON.parse(text) : null; } catch { j = null; } + if (resp.status === 429) { + markAccountFailure(account, 429, 'check'); + return { status: 'WAIT', email: '' }; + } + const code = j && (j.code ?? j.data?.code); + if (resp.status === 401 || resp.status === 403 || [40003, 40300, 40301].includes(Number(code))) { + return { status: 'INVALID', email: '' }; + } + if (resp.ok && j && j.data && j.data.biz_data) { + return { status: 'OK', email: j.data.biz_data.email || '' }; + } + return { status: 'ERROR', email: '' }; + } catch { + // Network error or 15s timeout — treat as unknown, do not crash the route. + return { status: 'ERROR', email: '' }; + } +} async function readDeepSeekJsonResponse(resp, label, account) { const text = await resp.text(); let json = null; @@ -1061,6 +1203,22 @@ function formatMessages(messages, tools) { return { prompt: conversation.trim(), systemPrompt: systemPrompt.trim() }; } +// Account management is allowed from the local machine only. +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 guard for account management: reject cross-site requests. A browser always +// sends Origin (and Referer) on a cross-origin request, so reject when the source +// host does not match the server host. Non-browser clients (curl/scripts) send +// neither and are not a CSRF vector, so they pass. +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; } +} + // === HTTP Server === const server = http.createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); @@ -1136,6 +1294,95 @@ 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 ok = accounts.find(a => accountStatusStr(a) === 'OK'); + const tokenExp = ok ? decodeTokenInfo(ok.config.token).exp : null; + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + config_ready: hasAuthConfig(), + accounts_total: accounts.length, + accounts_online: accounts.filter(a => accountStatusStr(a) === 'OK').length, + accounts_limited: accounts.filter(a => accountStatusStr(a) === 'WAIT').length, + token_exp: tokenExp, + has_token: accounts.some(a => a.config.token), + has_cookie: accounts.some(a => a.config.cookie), + has_hif: accounts.some(a => a.config.hif_dliq || a.config.hif_leim), + })); + return; + } + + // ── DeepSeek account management (localhost only) ── + 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: 'Available from localhost only' })); return; } + if (isCrossOrigin(req)) { res.writeHead(403, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Cross-origin request rejected' })); return; } + + // GET /api/accounts — list with statuses + if (req.method === 'GET' && url.pathname === '/api/accounts') { + res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ accounts: listAccountsPublic() })); return; + } + + // POST /api/accounts/import — add an account from cURL/HAR (request body) + 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 prevWasm = accounts.find(a => a.config.wasmUrl)?.config.wasmUrl || DEFAULT_WASM; + const parsed = finalizeAuth(parseAuthInput(body), prevWasm); + if (parsed.error) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(parsed)); return; } + const r = addAccountFromAuth(parsed); + 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: 'Import error: ' + e.message })); } + }); + return; + } + + // POST /api/accounts/:id/check — real liveness check via GET users/current + const mCheck = url.pathname.match(/^\/api\/accounts\/(account_[A-Za-z0-9]+)\/check$/); + if (req.method === 'POST' && mCheck) { + const id = mCheck[1]; + const account = accounts.find(a => a.id === id); + if (!account) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Account not found' })); return; } + const { status, email } = await checkAccountLive(account); + // On success, persist a freshly discovered email both in memory and (for + // managed files) on disk, so it survives the next pool reload. + if (status === 'OK' && email) { + account.config.email = email; + if (isManagedFile(account.file)) { + try { fs.writeFileSync(account.file, JSON.stringify(account.config, null, 2), { mode: 0o600 }); } + catch (e) { console.error(`[account:${id}] could not persist email: ${e.message}`); } + } + } + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ id, status, email: account.config.email || '', exp: decodeTokenInfo(account.config.token).exp })); + return; + } + + // DELETE /api/accounts/:id or POST /api/accounts/:id/delete + const mDel = url.pathname.match(/^\/api\/accounts\/(account_[A-Za-z0-9]+)(\/delete)?$/); + if (mDel && (req.method === 'DELETE' || (req.method === 'POST' && mDel[2]))) { + const r = deleteAccountById(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: 'Unknown accounts endpoint' })); return; + } + const apiMode = url.pathname === '/v1/messages' ? 'anthropic' : (url.pathname === '/v1/responses' ? 'responses' : 'openai'); From c3b3fa252c18eed5012dffd8e68f66d171756f9c Mon Sep 17 00:00:00 2001 From: dansc Date: Tue, 9 Jun 2026 18:59:52 +0300 Subject: [PATCH 2/2] feat: dashboard/extension polish + per-account label - dashboard: model picker, inline delete, copy, retry, auto-grow, and a per-account label with inline edit - chrome extension: icons, server-status indicator, dashboard button, label - parseAuth: handle Chrome "Copy as cURL (bash)" ANSI-C headers - server: GET /api/accounts exposes label; POST /api/accounts/:id/label sets it (localhost + CSRF guarded, persisted to the managed account file) --- chrome-extension/icons/icon128.png | Bin 0 -> 510 bytes chrome-extension/icons/icon16.png | Bin 0 -> 107 bytes chrome-extension/icons/icon48.png | Bin 0 -> 195 bytes chrome-extension/manifest.json | 8 +-- chrome-extension/popup.html | 18 ++++++- chrome-extension/popup.js | 15 +++++- lib/parseAuth.js | 10 ++-- public/dashboard.html | 75 ++++++++++++++++++++++++++--- server.js | 25 ++++++++++ 9 files changed, 135 insertions(+), 16 deletions(-) create mode 100644 chrome-extension/icons/icon128.png create mode 100644 chrome-extension/icons/icon16.png create mode 100644 chrome-extension/icons/icon48.png diff --git a/chrome-extension/icons/icon128.png b/chrome-extension/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..ccce26717203865b0ca26c9ac51243db82f7def7 GIT binary patch literal 510 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU_9#S;uunK>+RixzRrO>E*DoS zyI$aO4HofM3Upk;kyw51k=X;wAhQqga{c}CU&Y)R4k$7RFfchVa4;|`Ft9YBbJ&0V z`Sp*t;W6`mX@=Bh^Tv*gyA6+gxGRwKBag$d#=J#hpR`lY{a%H(_iX}?%auDm?iM`q z<1R0|HR7#J9i<-36t0a=gDv6Bo826ehjKm{ORP^S)KLIfa! z{6N7E!VlCDVlbA%2lYf4yRn{^8N_Zlz^(+OnB+Tw+Bj-7gcukUK5WbYNi+)zAGx&oW>1r=e=g|Y2EO(FPqV)al_xb*NlJI+q})KSs$!- zh}m~BlVLqm<~~PL21bSkWrha=xB{HXp}mS}TIoFh3kP>T0eQgF)z4*}Q$iB}GXI;p literal 0 HcmV?d00001 diff --git a/chrome-extension/icons/icon16.png b/chrome-extension/icons/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..14af7735722b945948d5e9fc855d604ab86d67b5 GIT binary patch literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`MxHK?Ar-fhCrrBZ+n+N{Vh-C6 z`G0+)!l?{;XLs-B4CrhOmd9Dx|g=UIFT6@O1TaS?83{ F1OUg!AN~LU literal 0 HcmV?d00001 diff --git a/chrome-extension/icons/icon48.png b/chrome-extension/icons/icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..dc6a5a2ee38c1bf62e991cdf12d4e93f97d1e6ad GIT binary patch literal 195 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtE>9Q7kcwMxZ|-JnP!M3bxVBj- z=jbBcQvp#HE_?WeW9;-Nhpv_Ne -

🔑 DeepSeek → FreeDeepseekAPI

+
+

🔑 DeepSeek → FreeDeepseekAPI

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

🔑 DeepSeek → FreeDeepseekAPI

Пул аккаунтов - + + + +
diff --git a/chrome-extension/popup.js b/chrome-extension/popup.js index e2c4127..423ea09 100644 --- a/chrome-extension/popup.js +++ b/chrome-extension/popup.js @@ -6,12 +6,15 @@ 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 setSrvDot(online) { const d = $('srvDot'); if (d) { d.className = 'srv-dot ' + (online ? 'online' : 'offline'); d.title = online ? 'Сервер онлайн (:9655)' : 'Сервер недоступен (:9655)'; } } +function setCredButtons(has) { for (const id of ['btnAdd', 'btnCopy', 'btnSave']) { const b = $(id); if (b) b.disabled = !has; } } function render(cap) { if (!cap || !cap.token || !cap.cookie) { setStatus('warn', '⚠️ Откройте chat.deepseek.com и ОТПРАВЬТЕ любое сообщение, затем нажмите кнопку.'); $('jsonPreview').textContent = '{ }'; $('detail').textContent = 'Креды появятся после запроса к DeepSeek'; + setCredButtons(false); return null; } const auth = { token: cap.token, hif_dliq: cap.hif_dliq || '', hif_leim: cap.hif_leim || '', cookie: cap.cookie, wasmUrl: cap.wasmUrl }; @@ -23,6 +26,7 @@ function render(cap) { }, null, 2); setStatus('ok', '✅ Перехвачено: token + cookie' + (auth.hif_leim ? ' + hif' : '') + ' — готово'); $('detail').textContent = cap._t ? ('Обновлено: ' + new Date(cap._t).toLocaleTimeString()) : ''; + setCredButtons(true); return auth; } @@ -55,7 +59,7 @@ function renderPool(list) { idEl.className = 'id'; idEl.textContent = a.id; const emailEl = document.createElement('span'); - emailEl.className = 'email'; emailEl.textContent = a.email || '—'; emailEl.title = a.email || ''; + emailEl.className = 'email'; emailEl.textContent = a.label || a.email || '—'; emailEl.title = a.email || a.label || ''; const badge = document.createElement('span'); badge.className = 'badge ' + (a.status || '').toLowerCase(); badge.textContent = a.status || '—'; @@ -73,8 +77,10 @@ async function loadPool() { try { const r = await fetch(API_BASE + '/api/accounts'); const j = await r.json(); + setSrvDot(true); renderPool(j.accounts || []); } catch { + setSrvDot(false); $('poolTitle').textContent = 'Пул аккаунтов'; setEmpty('FreeDeepseekAPI недоступен на :9655'); } @@ -100,6 +106,12 @@ async function deleteAccount(id) { // делегирование кликов в панели (check / удаление с инлайн-подтверждением) $('pool').addEventListener('click', (e) => { + const emailEl = e.target.closest('.email'); + if (emailEl && emailEl.textContent && emailEl.textContent !== '—') { + const full = emailEl.title || emailEl.textContent; + navigator.clipboard.writeText(full).then(() => { const o = emailEl.textContent; emailEl.textContent = '✓ скопировано'; setTimeout(() => { emailEl.textContent = o; }, 900); }); + return; + } 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; @@ -120,6 +132,7 @@ async function checkAll() { $('btnCheckAll').disabled = false; } $('btnCheckAll').addEventListener('click', checkAll); +$('btnDashboard').addEventListener('click', () => { chrome.tabs.create({ url: API_BASE + '/dashboard' }); }); // ── Главная кнопка — добавить перехваченные креды + авто-валидация ── $('btnAdd').addEventListener('click', async () => { diff --git a/lib/parseAuth.js b/lib/parseAuth.js index b333b9a..b406925 100644 --- a/lib/parseAuth.js +++ b/lib/parseAuth.js @@ -8,14 +8,15 @@ const WASM_DEFAULT = 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm'; -// -H 'name: value' | -H "name: value" | --header ... +// -H 'name: value' | -H "name: value" | -H $'name: value' (Chrome bash ANSI-C) | --header ... +// Опциональный \$? перед кавычкой покрывает форму "Copy as cURL (bash)". function extractHeadersFromCurl(curl) { const headers = {}; - const re = /(?:-H|--header)\s+(['"])(.+?):\s?([\s\S]*?)\1(?=\s|$)/g; + 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|$)/); + const mc = curl.match(/(?:-b|--cookie)\s+\$?(['"])([\s\S]*?)\1(?=\s|$)/); if (mc) headers['cookie'] = mc[2].trim(); } return headers; @@ -31,7 +32,8 @@ function fromHeaders(h) { } function parseCurl(curl) { - const s = String(curl || ''); + // снять bash line-continuations (\ + перевод строки), которые Chrome вставляет в "Copy as cURL" + const s = String(curl || '').replace(/\\\r?\n/g, ' '); const r = fromHeaders(extractHeadersFromCurl(s)); const wm = s.match(/https?:\/\/[^\s'"]*sha3[^\s'"]*\.wasm/i); r.wasmUrl = wm ? wm[0] : ''; diff --git a/public/dashboard.html b/public/dashboard.html index 8725ae3..8ce1a9c 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -72,6 +72,8 @@ button:disabled{opacity:.4;cursor:default;transform:none} button.primary{background:var(--accent-press);border-color:var(--accent-deep);color:#fff;font-weight:600} button.primary:hover{background:var(--accent);color:#fff} + button.danger{background:var(--err);border-color:var(--err);color:#fff;font-weight:600} + button.danger:hover{background:#ff7b72;border-color:#ff7b72;color:#fff} button.sm{padding:5px 9px;font-size:var(--fs-xs)} button svg.ic{font-size:15px} button.toggle.on{background:rgba(77,107,254,.16);border-color:var(--accent);color:var(--accent)} @@ -97,6 +99,7 @@ .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)} + .acc-label{font-weight:600;color:var(--accent);font-size:var(--fs-sm)} .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} @@ -211,6 +214,7 @@

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

Аккаунты DeepSeek +

загрузка…
@@ -339,12 +343,17 @@

Модели (Пока нет аккаунтов. Добавьте первый через импорт ниже.

'; 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)} + const email=a.email?`${esc(a.email)}`:''; + const labelHtml=a.label?`${esc(a.label)}`:''; + return `
${esc(a.id)}${labelHtml}${accStLabel(a.status)}${email}…${esc(a.preview||'')}${esc(meta)} +
`; }).join(''); - }catch{ list.innerHTML='
Не удалось загрузить аккаунты
'; } + setAccUpdated(); + }catch{ list.innerHTML='
Не удалось загрузить аккаунты.
'; } } +function setAccUpdated(){ const e=$('#accUpdated'); if(e) e.textContent='обновлено '+new Date().toLocaleTimeString('ru-RU',{hour:'2-digit',minute:'2-digit'}); } $('#checkAllAcc').onclick=async()=>{ const b=$('#checkAllAcc'); b.disabled=true; try{ const j=await (await fetch(origin+'/api/accounts')).json(); @@ -361,9 +370,25 @@

Модели (Модели (R':'')+(c.search?'S':''); return `${esc(m.id)}${caps}`; }).join('')||'не удалось загрузить'; - }catch{ list.innerHTML='не удалось загрузить'; } + }catch{ list.innerHTML='не удалось загрузить. '; } +} +// #chatModel наполняется реальными моделями из /v1/models; выбор сохраняется в localStorage +async function populateChatModels(){ + const sel=$('#chatModel'); if(!sel) return; + try{ + const j=await (await fetch(apiBase+'/models')).json(); + const data=j.data||[]; if(!data.length) return; + const saved=localStorage.getItem('ds_model'); + sel.innerHTML=data.map(m=>``).join(''); + if(saved && data.some(m=>m.id===saved)) sel.value=saved; + }catch{ /* оставляем статичный fallback-список */ } } document.addEventListener('click', e=>{ const c=e.target.closest('.chip[data-model]'); if(c) copyText(c.dataset.model); }); @@ -400,7 +436,14 @@

Модели (Модели (Модели (