From ebc7911a3fdac4aa7e303884527569f88c31623b Mon Sep 17 00:00:00 2001
From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com>
Date: Mon, 8 Jun 2026 07:13:44 +0000
Subject: [PATCH 1/2] Remove all ForgetMeAI watermarks and branding
- Remove FORGETMEAI_WATERMARK constant and formatWatermark function from server.js
- Remove watermark fields from all API responses (health, model-capabilities, chat completions, Anthropic, Responses)
- Remove watermark console.log messages from banner, menu, and server startup
- Remove WATERMARK constant and watermark() function from scripts/auth.js
- Remove ForgetMeAI Telegram link and footer from README.md
- Update GitHub URLs from ForgetMeAI org to operatorpuar in README.md and package.json
Co-authored-by: operatorpuar <235687124+operatorpuar@users.noreply.github.com>
---
README.md | 10 ++--------
package.json | 2 +-
scripts/auth.js | 5 -----
server.js | 15 +++------------
4 files changed, 6 insertions(+), 26 deletions(-)
diff --git a/README.md b/README.md
index af4a7f4..70c5b68 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
@@ -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
@@ -355,7 +353,3 @@ FreeDeepseekAPI — экспериментальный web-chat proxy для л
4. если проблема сохраняется — вероятно, DeepSeek изменил внутренний Web API.
---
-
-
- ForgetMeAI · Telegram
-
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..1d795d6 100755
--- a/scripts/auth.js
+++ b/scripts/auth.js
@@ -7,14 +7,11 @@ 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 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; }
@@ -42,7 +39,6 @@ function removeLocalAuth() {
function printHelp() {
divider();
console.log('FreeDeepseekAPI — управление DeepSeek Web login');
- console.log(watermark());
divider();
console.log('Опции:');
console.log(' --login Открыть Chrome и обновить auth');
@@ -55,7 +51,6 @@ function printHelp() {
async function menu() {
while (true) {
divider();
- console.log(watermark());
status();
divider();
console.log('Меню:');
diff --git a/server.js b/server.js
index 190f740..7e28711 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) {
@@ -545,7 +542,6 @@ function buildToolCallResponse(toolCall, model = 'deepseek-default', prompt = ''
finish_reason: 'tool_calls'
}],
usage: buildUsage(prompt, '', reasoningContent),
- watermark: FORGETMEAI_WATERMARK
};
}
@@ -563,7 +559,6 @@ function buildTextResponse(content, prompt, model = 'deepseek-default', reasonin
finish_reason: 'stop'
}],
usage: buildUsage(prompt, content, reasoningContent),
- watermark: FORGETMEAI_WATERMARK
};
}
@@ -701,7 +696,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 +773,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 +952,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, config_ready: hasAuthConfig() }));
return;
}
@@ -973,7 +966,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;
}
@@ -1328,7 +1321,7 @@ async function runAuthScript() {
}
function printStatus() {
- console.log(`\n${formatWatermark()}`);
+ console.log(`\nFreeDeepseekAPI`);
console.log(`Auth: ${hasAuthConfig() ? '✅ OK' : '❌ не найден deepseek-auth.json'}`);
console.log(`Auth file: ${DS_CONFIG_PATH}`);
console.log(`Рабочие модели: ${SUPPORTED_MODEL_IDS.join(', ')}`);
@@ -1344,7 +1337,6 @@ async function showStartupMenu() {
while (true) {
printStatus();
console.log('\n=== Меню ===');
- console.log(`ForgetMeAI: ${FORGETMEAI_WATERMARK}`);
console.log('1 - Авторизоваться / обновить DeepSeek login');
console.log('2 - Показать модели и статусы');
console.log('3 - Запустить прокси (по умолчанию)');
@@ -1374,7 +1366,6 @@ async function main() {
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] 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');
From 36e9aac3e870f33028c70c9262b7725b16dbb811 Mon Sep 17 00:00:00 2001
From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com>
Date: Mon, 8 Jun 2026 08:45:56 +0000
Subject: [PATCH 2/2] Add multi-account support with round-robin balancing
- Replace single DS_CONFIG with account pool system
- Load accounts from: accounts/ dir, accounts.json, or legacy deepseek-auth.json
- Round-robin selection between available accounts
- Per-account rate limiting (30 req/min default, configurable via ACCOUNT_RATE_LIMIT)
- Per-account cooldown (60s) on errors (401/403/500)
- Per-account health stats (total/success/error counts, timestamps)
- Add GET /v1/accounts endpoint for pool status monitoring
- Add POST /v1/accounts/reload for hot-reload without restart
- Update auth scripts to support named accounts (--add )
- Add DEEPSEEK_ACCOUNT_NAME env var for auth script
- Add accounts.example.json template
- Add accounts/ directory with .gitkeep
- Update .gitignore to exclude real account files
- Update README with multi-account documentation
---
.gitignore | 3 +
README.md | 91 +++++++++++
accounts.example.json | 18 +++
accounts/.gitkeep | 0
scripts/auth.js | 151 ++++++++++++++----
scripts/deepseek_chrome_auth.js | 8 +-
server.js | 264 +++++++++++++++++++++++++++-----
7 files changed, 464 insertions(+), 71 deletions(-)
create mode 100644 accounts.example.json
create mode 100644 accounts/.gitkeep
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 70c5b68..0c6cc2d 100644
--- a/README.md
+++ b/README.md
@@ -279,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 |
@@ -288,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:
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/scripts/auth.js b/scripts/auth.js
index 1d795d6..511c410 100755
--- a/scripts/auth.js
+++ b/scripts/auth.js
@@ -6,70 +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');
+
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 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('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();
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 7e28711..3fef488 100755
--- a/server.js
+++ b/server.js
@@ -56,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",
@@ -68,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() {
@@ -114,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;
@@ -248,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.`);
@@ -268,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;
@@ -296,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,
@@ -319,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;
@@ -334,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,
@@ -344,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 ===
@@ -952,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', 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;
}
@@ -987,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';
@@ -1322,16 +1505,20 @@ async function runAuthScript() {
function printStatus() {
console.log(`\nFreeDeepseekAPI`);
- console.log(`Auth: ${hasAuthConfig() ? '✅ OK' : '❌ не найден deepseek-auth.json'}`);
- console.log(`Auth file: ${DS_CONFIG_PATH}`);
+ 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) {
@@ -1350,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;
@@ -1365,12 +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] 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');