diff --git a/.gitignore b/.gitignore index 2610c6d..75af01e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ node_modules/ *.log auth.json deepseek-auth.json +accounts.json +accounts/*.json +!accounts/.gitkeep .chrome-profile-deepseek/ .chrome-for-testing-profile-deepseek/ .chrome-for-testing-profile-deepseek.stale-*/ diff --git a/README.md b/README.md index af4a7f4..0c6cc2d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

- License MIT + License MIT Node.js 18 plus No npm dependencies OpenAI compatible @@ -26,8 +26,6 @@ FreeDeepseekAPI поднимает локальный API-сервер для ** > ⚠️ Это экспериментальный web-chat proxy. DeepSeek может менять внутренний Web API без предупреждения. Для production-кейсов надёжнее официальный платный API DeepSeek. -ForgetMeAI: https://t.me/forgetmeai - --- ## Навигация @@ -80,7 +78,7 @@ ForgetMeAI: https://t.me/forgetmeai ## ⚡ Быстрый старт ```bash -git clone https://github.com/ForgetMeAI/FreeDeepseekAPI.git +git clone https://github.com/operatorpuar/FreeDeepseekAPI.git cd FreeDeepseekAPI npm run auth npm start @@ -281,6 +279,8 @@ Search для Expert по remote config недоступен, поэтому `de | `GET` | `/` или `/health` | статус proxy | | `GET` | `/v1/models` | список рабочих OpenAI-compatible aliases | | `GET` | `/v1/model-capabilities` | полный маппинг aliases, real model, capabilities | +| `GET` | `/v1/accounts` | статус пула аккаунтов (health, rate limits) | +| `POST` | `/v1/accounts/reload` | горячая перезагрузка аккаунтов без рестарта | | `POST` | `/v1/chat/completions` | OpenAI-compatible Chat Completions | | `POST` | `/v1/messages` | Anthropic Messages API shim | | `POST` | `/v1/responses` | OpenAI Responses API shim | @@ -290,6 +290,95 @@ Search для Expert по remote config недоступен, поэтому `de --- +## 👥 Мульти-аккаунт + +Прокси поддерживает работу с несколькими DeepSeek-аккаунтами одновременно с round-robin балансировкой. + +### Добавление аккаунтов + +**Способ 1: через меню (рекомендуется)** + +```bash +npm run auth +# Выбрать пункт 2 — «Добавить новый аккаунт» +# Ввести имя аккаунта (например: work, personal, bot1) +# Залогиниться в открывшемся Chrome +``` + +**Способ 2: через CLI** + +```bash +# Добавить именованный аккаунт +npm run auth -- --add work +npm run auth -- --add personal +npm run auth -- --add bot1 +``` + +**Способ 3: вручную** + +Положите JSON-файлы в директорию `accounts/`: + +```bash +accounts/ +├── work.json +├── personal.json +└── bot1.json +``` + +Каждый файл — стандартный формат auth: + +```json +{ + "name": "work", + "token": "YOUR_TOKEN", + "hif_dliq": "", + "hif_leim": "", + "cookie": "ds_session_id=...; smidV2=...", + "wasmUrl": "https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm" +} +``` + +**Способ 4: accounts.json** + +Массив аккаунтов в одном файле (см. `accounts.example.json`). + +### Приоритет загрузки + +1. `accounts/*.json` — отдельные файлы (наивысший приоритет) +2. `accounts.json` — массив аккаунтов +3. `deepseek-auth.json` — legacy одиночный аккаунт (fallback) + +Дубликаты (по token) автоматически исключаются. + +### Балансировка + +- **Round-robin** между доступными аккаунтами +- **Rate limit**: до 30 запросов/мин на аккаунт (настраивается через `ACCOUNT_RATE_LIMIT`) +- **Cooldown**: 60 сек при ошибке (401/403/500) — аккаунт временно исключается +- **Автовосстановление**: после cooldown аккаунт возвращается в пул + +### Мониторинг + +```bash +# Статус всех аккаунтов +curl http://localhost:9655/v1/accounts + +# Горячая перезагрузка (добавили новый аккаунт без рестарта) +curl -X POST http://localhost:9655/v1/accounts/reload +``` + +### Переменные окружения + +| Переменная | По умолчанию | Описание | +| --- | --- | --- | +| `DEEPSEEK_ACCOUNTS_DIR` | `./accounts` | Директория с JSON-файлами аккаунтов | +| `DEEPSEEK_ACCOUNTS_PATH` | `./accounts.json` | Путь к массиву аккаунтов | +| `DEEPSEEK_AUTH_PATH` | `./deepseek-auth.json` | Legacy одиночный аккаунт | +| `ACCOUNT_RATE_LIMIT` | `30` | Макс. запросов на аккаунт в минуту | +| `DEEPSEEK_ACCOUNT_NAME` | — | Имя аккаунта при авторизации (для `npm run deepseek:auth`) | + +--- + ## 🖥 Open WebUI Base URL для Open WebUI в Docker: @@ -355,7 +444,3 @@ FreeDeepseekAPI — экспериментальный web-chat proxy для л 4. если проблема сохраняется — вероятно, DeepSeek изменил внутренний Web API. --- - -

- ForgetMeAI · Telegram -

diff --git a/accounts.example.json b/accounts.example.json new file mode 100644 index 0000000..63ae9b6 --- /dev/null +++ b/accounts.example.json @@ -0,0 +1,18 @@ +[ + { + "name": "account-1", + "token": "YOUR_DEEPSEEK_TOKEN_1", + "hif_dliq": "", + "hif_leim": "", + "cookie": "ds_session_id=YOUR_SESSION_ID_1; smidV2=YOUR_SMIDV2_1", + "wasmUrl": "https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm" + }, + { + "name": "account-2", + "token": "YOUR_DEEPSEEK_TOKEN_2", + "hif_dliq": "", + "hif_leim": "", + "cookie": "ds_session_id=YOUR_SESSION_ID_2; smidV2=YOUR_SMIDV2_2", + "wasmUrl": "https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm" + } +] diff --git a/accounts/.gitkeep b/accounts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json index 3e2e88c..8fc90a0 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,6 @@ }, "repository": { "type": "git", - "url": "https://github.com/ForgetMeAI/FreeDeepseekAPI.git" + "url": "https://github.com/operatorpuar/FreeDeepseekAPI.git" } } diff --git a/scripts/auth.js b/scripts/auth.js index 5c802cf..511c410 100755 --- a/scripts/auth.js +++ b/scripts/auth.js @@ -6,75 +6,155 @@ const { spawnSync } = require('child_process'); const ROOT = path.resolve(__dirname, '..'); const AUTH_PATH = process.env.DEEPSEEK_AUTH_PATH || path.join(ROOT, 'deepseek-auth.json'); +const ACCOUNTS_DIR = path.join(ROOT, 'accounts'); const PROFILE_DIR = process.env.DEEPSEEK_CHROME_PROFILE || path.join(ROOT, '.chrome-for-testing-profile-deepseek'); -const WATERMARK = 't.me/forgetmeai'; function prompt(question) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans); })); } function divider() { console.log('======================================================'); } -function watermark(prefix = 'ForgetMeAI') { return `${prefix}: ${WATERMARK}`; } + function loadAuth() { try { return JSON.parse(fs.readFileSync(AUTH_PATH, 'utf8')); } catch { return null; } } + +function listAccounts() { + const accounts = []; + // Legacy single file + const legacy = loadAuth(); + if (legacy && legacy.token) { + accounts.push({ name: 'default (deepseek-auth.json)', token: legacy.token, cookie: legacy.cookie, source: AUTH_PATH }); + } + // accounts/ directory + if (fs.existsSync(ACCOUNTS_DIR)) { + const files = fs.readdirSync(ACCOUNTS_DIR).filter(f => f.endsWith('.json')).sort(); + for (const file of files) { + try { + const data = JSON.parse(fs.readFileSync(path.join(ACCOUNTS_DIR, file), 'utf8')); + if (data.token) { + accounts.push({ name: data.name || path.basename(file, '.json'), token: data.token, cookie: data.cookie, source: path.join(ACCOUNTS_DIR, file) }); + } + } catch {} + } + } + // accounts.json + const accountsJsonPath = path.join(ROOT, 'accounts.json'); + if (fs.existsSync(accountsJsonPath)) { + try { + const data = JSON.parse(fs.readFileSync(accountsJsonPath, 'utf8')); + const arr = Array.isArray(data) ? data : (data.accounts || []); + arr.forEach((a, i) => { + if (a && a.token) accounts.push({ name: a.name || `accounts.json[${i}]`, token: a.token, cookie: a.cookie, source: accountsJsonPath }); + }); + } catch {} + } + return accounts; +} + function status() { - const auth = loadAuth(); - console.log('\nDeepSeek аккаунт:'); - if (!auth) { - console.log(' ❌ deepseek-auth.json не найден'); + const accounts = listAccounts(); + console.log(`\nDeepSeek аккаунты: ${accounts.length} шт.`); + if (accounts.length === 0) { + console.log(' ❌ Нет аккаунтов. Используйте пункт 1 или 2 для добавления.'); } else { - console.log(` ✅ auth file: ${AUTH_PATH}`); - console.log(` token: ${auth.token ? 'OK (' + String(auth.token).length + ' chars)' : 'MISSING'}`); - console.log(` cookies: ${auth.cookie ? 'OK' : 'MISSING'}`); - console.log(` Chrome profile: ${fs.existsSync(PROFILE_DIR) ? PROFILE_DIR : 'не найден'}`); + accounts.forEach((acc, i) => { + const tokenOk = acc.token ? '✅' : '❌'; + const cookieOk = acc.cookie ? '✅' : '❌'; + console.log(` ${i + 1}. ${acc.name} — token: ${tokenOk} cookie: ${cookieOk}`); + console.log(` source: ${acc.source}`); + }); } + console.log(` Chrome profile: ${fs.existsSync(PROFILE_DIR) ? PROFILE_DIR : 'не найден'}`); } -function runDirectAuth() { + +function runDirectAuth(accountName) { const script = path.join(__dirname, 'deepseek_chrome_auth.js'); - return spawnSync(process.execPath, [script], { stdio: 'inherit', env: process.env }).status === 0; + const env = { ...process.env }; + if (accountName) env.DEEPSEEK_ACCOUNT_NAME = accountName; + return spawnSync(process.execPath, [script], { stdio: 'inherit', env }).status === 0; } -function removeLocalAuth() { - if (fs.existsSync(AUTH_PATH)) fs.rmSync(AUTH_PATH, { force: true }); - console.log('Удалён deepseek-auth.json. Chrome profile оставлен, чтобы не разлогинивать браузер без нужды.'); + +function removeAccount(filePath) { + if (fs.existsSync(filePath)) fs.rmSync(filePath, { force: true }); + console.log(`Удалён: ${filePath}`); } + function printHelp() { divider(); - console.log('FreeDeepseekAPI — управление DeepSeek Web login'); - console.log(watermark()); + console.log('FreeDeepseekAPI — управление DeepSeek Web аккаунтами'); divider(); console.log('Опции:'); - console.log(' --login Открыть Chrome и обновить auth'); - console.log(' --status Показать статус auth'); - console.log(' --remove Удалить локальный deepseek-auth.json'); - console.log(' --help Справка'); - console.log('Без опций запускается интерактивное меню.'); + console.log(' --login Авторизовать аккаунт (сохранить в deepseek-auth.json)'); + console.log(' --add Добавить именованный аккаунт в accounts/.json'); + console.log(' --status Показать все аккаунты'); + console.log(' --remove Удалить deepseek-auth.json'); + console.log(' --help Справка'); + console.log(''); + console.log('Мульти-аккаунт:'); + console.log(' DEEPSEEK_ACCOUNT_NAME=my-acc npm run deepseek:auth'); + console.log(' Или через меню: пункт 2'); divider(); } + async function menu() { while (true) { divider(); - console.log(watermark()); status(); divider(); console.log('Меню:'); - console.log('1 - Авторизоваться / обновить DeepSeek login'); - console.log('2 - Показать статус'); - console.log('3 - Удалить локальный auth файл'); - console.log('4 - Выход'); - const choice = (await prompt('Ваш выбор (Enter = 4): ')) || '4'; - if (choice === '1') runDirectAuth(); - else if (choice === '2') { status(); await prompt('\nНажмите Enter, чтобы вернуться в меню...'); } - else if (choice === '3') removeLocalAuth(); - else if (choice === '4') break; + console.log('1 - Авторизовать / обновить основной аккаунт (deepseek-auth.json)'); + console.log('2 - Добавить новый аккаунт (accounts/.json)'); + console.log('3 - Показать все аккаунты'); + console.log('4 - Удалить аккаунт'); + console.log('5 - Выход'); + const choice = (await prompt('Ваш выбор (Enter = 5): ')) || '5'; + if (choice === '1') { + runDirectAuth(); + } else if (choice === '2') { + const name = await prompt('Имя нового аккаунта (латиница, без пробелов): '); + if (!name || !/^[\w-]+$/.test(name)) { + console.log('Невалидное имя. Используйте только буквы, цифры, дефис, подчёркивание.'); + continue; + } + runDirectAuth(name); + } else if (choice === '3') { + status(); + await prompt('\nНажмите Enter, чтобы вернуться в меню...'); + } else if (choice === '4') { + const accounts = listAccounts(); + if (accounts.length === 0) { console.log('Нет аккаунтов для удаления.'); continue; } + accounts.forEach((acc, i) => console.log(` ${i + 1}. ${acc.name} (${acc.source})`)); + const idx = await prompt('Номер аккаунта для удаления (Enter = отмена): '); + const num = parseInt(idx, 10); + if (num >= 1 && num <= accounts.length) { + removeAccount(accounts[num - 1].source); + } + } else if (choice === '5') { + break; + } } } + (async () => { - const args = new Set(process.argv.slice(2)); - if (args.has('--help') || args.has('-h')) return printHelp(); - if (args.has('--login') || args.has('--add') || args.has('--relogin')) return void runDirectAuth(); - if (args.has('--status') || args.has('--list')) return status(); - if (args.has('--remove')) return removeLocalAuth(); + const args = process.argv.slice(2); + const argsSet = new Set(args); + if (argsSet.has('--help') || argsSet.has('-h')) return printHelp(); + if (argsSet.has('--login') || argsSet.has('--relogin')) return void runDirectAuth(); + if (argsSet.has('--add')) { + const nameIdx = args.indexOf('--add') + 1; + const name = args[nameIdx] || ''; + if (!name || !/^[\w-]+$/.test(name)) { + console.error('Usage: npm run auth -- --add '); + process.exit(1); + } + return void runDirectAuth(name); + } + if (argsSet.has('--status') || argsSet.has('--list')) return status(); + if (argsSet.has('--remove')) { + removeAccount(AUTH_PATH); + return; + } await menu(); })(); diff --git a/scripts/deepseek_chrome_auth.js b/scripts/deepseek_chrome_auth.js index 4d90b9f..654fa3c 100755 --- a/scripts/deepseek_chrome_auth.js +++ b/scripts/deepseek_chrome_auth.js @@ -26,7 +26,11 @@ const qwenRepoRoot = path.resolve(repoRoot, '..', 'FreeQwenApi'); const profileDir = process.env.DEEPSEEK_CHROME_PROFILE || path.join(repoRoot, '.chrome-for-testing-profile-deepseek'); // Use a dedicated default port so an older normal-Chrome auth window on 9333 is not reused. const port = Number(process.env.DEEPSEEK_CHROME_PORT || 9334); -const outPath = process.env.DEEPSEEK_AUTH_PATH || path.join(repoRoot, 'deepseek-auth.json'); +// If DEEPSEEK_ACCOUNT_NAME is set, save to accounts/.json for multi-account mode +const accountName = process.env.DEEPSEEK_ACCOUNT_NAME || ''; +const outPath = process.env.DEEPSEEK_AUTH_PATH || (accountName + ? path.join(repoRoot, 'accounts', `${accountName}.json`) + : path.join(repoRoot, 'deepseek-auth.json')); const url = 'https://chat.deepseek.com/'; const reuseChrome = /^(1|true|yes|on)$/i.test(process.env.DEEPSEEK_REUSE_CHROME || ''); const keepProfile = /^(1|true|yes|on)$/i.test(process.env.DEEPSEEK_KEEP_CHROME_PROFILE || ''); @@ -268,6 +272,8 @@ async function main() { await sleep(500); } const { href, cookiesCount, ...persisted } = auth; + if (accountName) persisted.name = accountName; + fs.mkdirSync(path.dirname(outPath), { recursive: true }); fs.writeFileSync(outPath, JSON.stringify(persisted, null, 2)); console.log(`[auth] Saved: ${outPath}`); console.log(`[auth] page: ${href || 'unknown'}`); diff --git a/server.js b/server.js index 190f740..3fef488 100755 --- a/server.js +++ b/server.js @@ -30,10 +30,8 @@ const SERVER_PUBLIC_IP = (() => { return 'localhost'; })(); -const FORGETMEAI_WATERMARK = 't.me/forgetmeai'; const PORT = Number(process.env.PORT || 9655); const HOST = process.env.HOST || '0.0.0.0'; -function formatWatermark(prefix = 'ForgetMeAI') { return `${prefix}: ${FORGETMEAI_WATERMARK}`; } function printBanner() { console.log(` ███████ ██████ ███████ ███████ ██████ ███████ ███████ ███████ ██ ██ @@ -43,7 +41,6 @@ function printBanner() { ██ ██ ██ ███████ ███████ ██████ ███████ ███████ ███████ ██ ██ FreeDeepseekAPI — API-прокси для DeepSeek Web Chat - ${formatWatermark()} `); } function prompt(question) { @@ -59,11 +56,23 @@ const MAX_HISTORY_CHARS = 10000; const MAX_MESSAGE_DEPTH = 100; // auto-reset after this many messages const SESSION_TTL_MS = 2 * 60 * 60 * 1000; // 2 hours -// === DeepSeek Web API Config — loaded from external config file === +// === Multi-Account Pool === +// Accounts are loaded from: +// 1. accounts/ directory (each .json file = one account) +// 2. accounts.json (array of account objects) +// 3. deepseek-auth.json (legacy single account, fallback) +const ACCOUNTS_DIR = process.env.DEEPSEEK_ACCOUNTS_DIR || path.join(__dirname, 'accounts'); +const ACCOUNTS_JSON_PATH = process.env.DEEPSEEK_ACCOUNTS_PATH || path.join(__dirname, 'accounts.json'); const DS_CONFIG_PATH = process.env.DEEPSEEK_AUTH_PATH || path.join(__dirname, 'deepseek-auth.json'); -let DS_CONFIG = {}; -let BASE_HEADERS = {}; -function buildBaseHeaders() { +const ACCOUNT_COOLDOWN_MS = 60 * 1000; // 1 minute cooldown on error +const ACCOUNT_RATE_LIMIT_WINDOW = 60 * 1000; // rate limit window (ms) +const ACCOUNT_MAX_REQUESTS_PER_WINDOW = Number(process.env.ACCOUNT_RATE_LIMIT || 30); + +// Account pool: each entry has { config, headers, name, stats } +const accountPool = []; +let roundRobinIndex = 0; + +function buildHeadersForAccount(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,33 +80,157 @@ 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 ${config.token || ''}`, + "x-hif-dliq": config.hif_dliq || '', + "x-hif-leim": config.hif_leim || '', "Origin": "https://chat.deepseek.com", "Referer": "https://chat.deepseek.com/", - "Cookie": DS_CONFIG.cookie || '', + "Cookie": config.cookie || '', "Content-Type": "application/json", }; } -function loadDeepSeekConfig({ fatal = true } = {}) { + +function createAccountEntry(config, name) { + return { + config, + headers: buildHeadersForAccount(config), + name: name || config.name || `account-${accountPool.length + 1}`, + stats: { + totalRequests: 0, + successCount: 0, + errorCount: 0, + lastUsed: null, + lastError: null, + cooldownUntil: null, + requestTimestamps: [], // for rate limiting + }, + }; +} + +function loadAccountsFromDir() { + if (!fs.existsSync(ACCOUNTS_DIR)) return []; + const files = fs.readdirSync(ACCOUNTS_DIR).filter(f => f.endsWith('.json')).sort(); + const accounts = []; + for (const file of files) { + try { + const raw = fs.readFileSync(path.join(ACCOUNTS_DIR, file), 'utf8'); + const config = JSON.parse(raw); + if (config.token && config.cookie) { + accounts.push(createAccountEntry(config, path.basename(file, '.json'))); + } + } catch (e) { + console.log(`[DS-API] Warning: failed to load account from ${file}: ${e.message}`); + } + } + return accounts; +} + +function loadAccountsFromJson() { + if (!fs.existsSync(ACCOUNTS_JSON_PATH)) return []; try { - const raw = fs.readFileSync(DS_CONFIG_PATH, 'utf8'); - DS_CONFIG = JSON.parse(raw); - BASE_HEADERS = buildBaseHeaders(); - console.log(`[DS-API] Loaded auth config from ${DS_CONFIG_PATH}`); - return true; + const raw = fs.readFileSync(ACCOUNTS_JSON_PATH, 'utf8'); + const data = JSON.parse(raw); + const arr = Array.isArray(data) ? data : (data.accounts || []); + return arr + .filter(c => c && c.token && c.cookie) + .map((c, i) => createAccountEntry(c, c.name || `account-${i + 1}`)); } catch (e) { - DS_CONFIG = {}; - BASE_HEADERS = buildBaseHeaders(); - if (fatal) { - console.error(`[DS-API] FATAL: Could not load auth config: ${e.message}`); - process.exit(1); + console.log(`[DS-API] Warning: failed to load accounts.json: ${e.message}`); + return []; + } +} + +function loadLegacySingleConfig() { + try { + const raw = fs.readFileSync(DS_CONFIG_PATH, 'utf8'); + const config = JSON.parse(raw); + if (config.token && config.cookie) { + return [createAccountEntry(config, 'default')]; + } + } catch (e) {} + return []; +} + +function loadAllAccounts() { + accountPool.length = 0; + // Priority: accounts/ dir > accounts.json > legacy single file + const fromDir = loadAccountsFromDir(); + const fromJson = loadAccountsFromJson(); + const fromLegacy = loadLegacySingleConfig(); + + // Deduplicate by token + const seen = new Set(); + for (const acc of [...fromDir, ...fromJson, ...fromLegacy]) { + const key = acc.config.token; + if (!seen.has(key)) { + seen.add(key); + accountPool.push(acc); } - return false; } + if (accountPool.length > 0) { + console.log(`[DS-API] Loaded ${accountPool.length} account(s): ${accountPool.map(a => a.name).join(', ')}`); + } + return accountPool.length > 0; +} + +function hasAuthConfig() { return accountPool.length > 0; } + +function isAccountAvailable(account) { + const now = Date.now(); + // Check cooldown + if (account.stats.cooldownUntil && now < account.stats.cooldownUntil) return false; + // Check rate limit + account.stats.requestTimestamps = account.stats.requestTimestamps.filter(t => now - t < ACCOUNT_RATE_LIMIT_WINDOW); + if (account.stats.requestTimestamps.length >= ACCOUNT_MAX_REQUESTS_PER_WINDOW) return false; + return true; +} + +function selectAccount() { + const available = accountPool.filter(isAccountAvailable); + if (available.length === 0) { + // All accounts on cooldown/rate-limited — pick the one with earliest cooldown expiry + const sorted = [...accountPool].sort((a, b) => (a.stats.cooldownUntil || 0) - (b.stats.cooldownUntil || 0)); + return sorted[0] || null; + } + // Round-robin among available accounts + roundRobinIndex = roundRobinIndex % available.length; + const selected = available[roundRobinIndex]; + roundRobinIndex = (roundRobinIndex + 1) % available.length; + return selected; +} + +function markAccountUsed(account) { + account.stats.totalRequests++; + account.stats.lastUsed = Date.now(); + account.stats.requestTimestamps.push(Date.now()); +} + +function markAccountSuccess(account) { + account.stats.successCount++; + account.stats.cooldownUntil = null; +} + +function markAccountError(account, error) { + account.stats.errorCount++; + account.stats.lastError = { message: error, time: Date.now() }; + account.stats.cooldownUntil = Date.now() + ACCOUNT_COOLDOWN_MS; + console.log(`[DS-API] Account "${account.name}" error: ${error}. Cooldown ${ACCOUNT_COOLDOWN_MS / 1000}s`); +} + +// Legacy compatibility aliases +let DS_CONFIG = {}; +let BASE_HEADERS = {}; +function loadDeepSeekConfig({ fatal = true } = {}) { + const loaded = loadAllAccounts(); + if (loaded) { + DS_CONFIG = accountPool[0].config; + BASE_HEADERS = accountPool[0].headers; + } else if (fatal) { + console.error('[DS-API] FATAL: No valid account configs found'); + process.exit(1); + } + return loaded; } -function hasAuthConfig() { return !!(DS_CONFIG.token && DS_CONFIG.cookie); } loadDeepSeekConfig({ fatal: false }); function createSession() { @@ -117,8 +250,9 @@ function getOrCreateAgentSession(agentId) { return sessions.get(agentId); } -async function solvePOW(challenge) { - const resp = await fetch(DS_CONFIG.wasmUrl); +async function solvePOW(challenge, account) { + const wasmUrl = account.config.wasmUrl || 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm'; + const resp = await fetch(wasmUrl); const wasmBytes = await resp.arrayBuffer(); const mod = await WebAssembly.instantiate(wasmBytes, { wbg: {} }); const e = mod.instance.exports; @@ -251,6 +385,15 @@ async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default') { const session = getOrCreateAgentSession(agentId); const agentTag = `[${agentId}]`; + // Select an account from the pool + const account = selectAccount(); + if (!account) { + throw new Error('No accounts available (all on cooldown or rate-limited)'); + } + const accountHeaders = account.headers; + markAccountUsed(account); + console.log(`${agentTag} Using account: ${account.name}`); + // 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.`); @@ -271,16 +414,16 @@ async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default') { } const cr = await fetch('https://chat.deepseek.com/api/v0/chat/create_pow_challenge', { - method: 'POST', headers: BASE_HEADERS, + method: 'POST', headers: accountHeaders, body: JSON.stringify({ target_path: '/api/v0/chat/completion' }) }); const chalJson = JSON.parse(await cr.text()); const challenge = chalJson.data.biz_data.challenge; - const answer = await solvePOW(challenge); + const answer = await solvePOW(challenge, account); 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: accountHeaders, body: '{}' }); const sessionData = await sr.json(); session.id = sessionData.data.biz_data.chat_session?.id || sessionData.data.biz_data.id; @@ -299,7 +442,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: { ...accountHeaders, 'X-DS-PoW-Response': powB64 }, body: JSON.stringify({ chat_session_id: session.id, parent_message_id: session.parentMessageId, @@ -322,7 +465,7 @@ async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default') { session.messageCount = 0; const sr2 = await fetch('https://chat.deepseek.com/api/v0/chat_session/create', { - method: 'POST', headers: BASE_HEADERS, body: '{}' + method: 'POST', headers: accountHeaders, body: '{}' }); const sessionData2 = await sr2.json(); session.id = sessionData2.data.biz_data.chat_session?.id || sessionData2.data.biz_data.id; @@ -337,7 +480,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: { ...accountHeaders, 'X-DS-PoW-Response': newPowB64 }, body: JSON.stringify({ chat_session_id: session.id, parent_message_id: null, @@ -347,11 +490,19 @@ async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default') { action: null, preempt: false, }) }); - return { resp: resp2, agentId }; + if (resp2.status !== 200) { + markAccountError(account, `HTTP ${resp2.status} on retry`); + } else { + markAccountSuccess(account); + } + return { resp: resp2, agentId, account }; } + markAccountError(account, `HTTP ${resp.status}`); + } else { + markAccountSuccess(account); } - return { resp, agentId }; + return { resp, agentId, account }; } // === Tool Calling Support === @@ -545,7 +696,6 @@ function buildToolCallResponse(toolCall, model = 'deepseek-default', prompt = '' finish_reason: 'tool_calls' }], usage: buildUsage(prompt, '', reasoningContent), - watermark: FORGETMEAI_WATERMARK }; } @@ -563,7 +713,6 @@ function buildTextResponse(content, prompt, model = 'deepseek-default', reasonin finish_reason: 'stop' }], usage: buildUsage(prompt, content, reasoningContent), - watermark: FORGETMEAI_WATERMARK }; } @@ -701,7 +850,6 @@ function toAnthropicResponse(openaiResp) { input_tokens: openaiResp.usage?.prompt_tokens || 0, output_tokens: openaiResp.usage?.completion_tokens || 0, }, - watermark: FORGETMEAI_WATERMARK, }; if (!hasToolCalls && msg.reasoning_content) response.reasoning_content = msg.reasoning_content; return response; @@ -779,7 +927,6 @@ function toResponsesResponse(openaiResp) { total_tokens: openaiResp.usage?.total_tokens || 0, output_tokens_details: { reasoning_tokens: openaiResp.usage?.completion_tokens_details?.reasoning_tokens || 0 }, }, - watermark: FORGETMEAI_WATERMARK, }; } @@ -959,7 +1106,7 @@ const server = http.createServer(async (req, res) => { // Health check if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/health')) { 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', models: SUPPORTED_MODEL_IDS, unsupported_models: Object.keys(MODEL_CONFIGS).filter(id => !MODEL_CONFIGS[id].supported), agents: sessions.size, accounts: accountPool.length, accounts_available: accountPool.filter(isAccountAvailable).length, config_ready: hasAuthConfig() })); return; } @@ -973,7 +1120,7 @@ const server = http.createServer(async (req, res) => { // Full mapping, including Web models observed but not currently usable through the direct API. if (req.method === 'GET' && (url.pathname === '/v1/model-capabilities' || url.pathname === '/api/model-capabilities')) { res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ object: 'model_capabilities', watermark: FORGETMEAI_WATERMARK, data: ALL_MODEL_CAPABILITIES })); + res.end(JSON.stringify({ object: 'model_capabilities', data: ALL_MODEL_CAPABILITIES })); return; } @@ -994,6 +1141,35 @@ const server = http.createServer(async (req, res) => { return; } + // Account pool status + if (req.method === 'GET' && url.pathname === '/v1/accounts') { + const now = Date.now(); + const accountList = accountPool.map(acc => ({ + name: acc.name, + available: isAccountAvailable(acc), + total_requests: acc.stats.totalRequests, + success_count: acc.stats.successCount, + error_count: acc.stats.errorCount, + last_used: acc.stats.lastUsed ? new Date(acc.stats.lastUsed).toISOString() : null, + last_error: acc.stats.lastError ? { message: acc.stats.lastError.message, time: new Date(acc.stats.lastError.time).toISOString() } : null, + cooldown_remaining_sec: acc.stats.cooldownUntil && acc.stats.cooldownUntil > now ? Math.round((acc.stats.cooldownUntil - now) / 1000) : 0, + requests_in_window: acc.stats.requestTimestamps.filter(t => now - t < ACCOUNT_RATE_LIMIT_WINDOW).length, + rate_limit: ACCOUNT_MAX_REQUESTS_PER_WINDOW, + })); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ accounts: accountList, total: accountList.length, available: accountList.filter(a => a.available).length })); + return; + } + + // Reload accounts (hot-reload without restart) + if (req.method === 'POST' && url.pathname === '/v1/accounts/reload') { + const prevCount = accountPool.length; + loadAllAccounts(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'reloaded', previous_count: prevCount, current_count: accountPool.length, accounts: accountPool.map(a => a.name) })); + return; + } + // Reset session for a specific agent (or all if no agent specified) if (req.method === 'POST' && url.pathname === '/reset-session') { const agentId = url.searchParams.get('agent') || 'default'; @@ -1328,23 +1504,26 @@ async function runAuthScript() { } function printStatus() { - console.log(`\n${formatWatermark()}`); - console.log(`Auth: ${hasAuthConfig() ? '✅ OK' : '❌ не найден deepseek-auth.json'}`); - console.log(`Auth file: ${DS_CONFIG_PATH}`); + console.log(`\nFreeDeepseekAPI`); + console.log(`Аккаунты: ${accountPool.length > 0 ? '✅ ' + accountPool.length + ' шт. (' + accountPool.map(a => a.name).join(', ') + ')' : '❌ нет аккаунтов'}`); + console.log(`Источники: accounts/ dir, accounts.json, deepseek-auth.json`); console.log(`Рабочие модели: ${SUPPORTED_MODEL_IDS.join(', ')}`); console.log('Нерабочие/скрытые aliases: ' + Object.keys(MODEL_CONFIGS).filter(id => !MODEL_CONFIGS[id].supported).join(', ')); console.log('Capabilities: GET /v1/model-capabilities'); + console.log('Account status: GET /v1/accounts'); } async function showStartupMenu() { if (isTruthy(process.env.SKIP_ACCOUNT_MENU) || isTruthy(process.env.NON_INTERACTIVE)) { - if (!hasAuthConfig()) loadDeepSeekConfig({ fatal: true }); + if (!hasAuthConfig()) { + console.error('[DS-API] FATAL: No valid account configs found'); + process.exit(1); + } return true; } while (true) { printStatus(); console.log('\n=== Меню ==='); - console.log(`ForgetMeAI: ${FORGETMEAI_WATERMARK}`); console.log('1 - Авторизоваться / обновить DeepSeek login'); console.log('2 - Показать модели и статусы'); console.log('3 - Запустить прокси (по умолчанию)'); @@ -1358,7 +1537,7 @@ async function showStartupMenu() { await prompt('\nНажмите Enter, чтобы вернуться в меню...'); } else if (choice === '3') { if (!hasAuthConfig()) { - console.log('Нужен deepseek-auth.json. Запустите пункт 1.'); + console.log('Нет аккаунтов. Положите auth-файлы в accounts/ или запустите пункт 1.'); continue; } return true; @@ -1373,13 +1552,15 @@ async function main() { const shouldStart = await showStartupMenu(); if (!shouldStart) process.exit(0); server.listen(PORT, HOST, () => { - console.log(`[DS-API] Server on http://${HOST}:${PORT} (multi-agent sessions enabled)`); - console.log(`[DS-API] ${formatWatermark()}`); + console.log(`[DS-API] Server on http://${HOST}:${PORT} (multi-account, multi-agent sessions)`); + console.log(`[DS-API] Accounts: ${accountPool.length} loaded (${accountPool.map(a => a.name).join(', ')})`); console.log('[DS-API] POST /v1/chat/completions (OpenAI Chat Completions, stream=true|false)'); console.log('[DS-API] POST /v1/messages — Anthropic Messages shim for Claude Code'); console.log('[DS-API] POST /v1/responses — OpenAI Responses API shim'); console.log('[DS-API] GET /v1/models — supported OpenAI-compatible models'); console.log('[DS-API] GET /v1/model-capabilities — real model mapping and capabilities'); + console.log('[DS-API] GET /v1/accounts — account pool status and health'); + console.log('[DS-API] POST /v1/accounts/reload — hot-reload accounts without restart'); console.log('[DS-API] GET /v1/sessions — list active agent sessions'); console.log('[DS-API] POST /reset-session?agent= — reset agent session'); console.log('[DS-API] POST /reset-session?agent=all — reset ALL sessions');