From 36357e03ce44fe7d138d46afe2437ad07203100b Mon Sep 17 00:00:00 2001 From: Mikhail Maksimov Date: Mon, 8 Jun 2026 12:26:51 +0500 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D1=8F?= =?UTF-8?q?=D0=B5=D1=82=20=D0=BA=D0=BE=D0=BD=D1=81=D0=BE=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=83=D1=8E=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 73 ++++++++ ecosystem.config.cjs | 11 ++ package.json | 2 +- scripts/auth.js | 76 ++++++++- scripts/deepseek_chrome_auth.js | 282 ++++++++++++++++++++++++++++--- scripts/deepseek_console_auth.js | 192 +++++++++++++++++++++ server.js | 7 +- 7 files changed, 616 insertions(+), 27 deletions(-) create mode 100644 ecosystem.config.cjs create mode 100644 scripts/deepseek_console_auth.js diff --git a/README.md b/README.md index af4a7f4..c0165aa 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ ForgetMeAI: https://t.me/forgetmeai - [Что это даёт](#-что-это-даёт) - [Возможности](#-возможности) - [Быстрый старт](#-быстрый-старт) +- [Фоновый запуск (PM2)](#-фоновый-запуск-pm2) - [Проверка работы](#-проверка-работы) - [Примеры запросов](#-примеры-запросов) - [Chat Completions](#chat-completions) @@ -116,6 +117,78 @@ http://localhost:9655 --- +## 🔄 Фоновый запуск (PM2) + +Для VPS/сервера без открытого терминала удобно запускать proxy через [PM2](https://pm2.keymetrics.io/). Сначала авторизуйтесь (`npm run auth`), затем поднимите сервер в фоне. + +При запуске через pm2 меню **пропускается автоматически** (нет интерактивного TTY). Явно можно задать `NON_INTERACTIVE=1` или `SKIP_ACCOUNT_MENU=1`. + +> **Важно:** флаг pm2 `--env` — это имя блока из `ecosystem.config.cjs` (`production`, `development`), а **не** `KEY=VALUE`. Запись `--env NON_INTERACTIVE=1` **не работает**. + +### Установка и быстрый старт + +```bash +npm install -g pm2 +cd FreeDeepseekAPI +npm run auth # deepseek-auth.json должен существовать + +pm2 delete deepseek-api 2>/dev/null || true +pm2 start server.js --name deepseek-api +pm2 save +``` + +С явной переменной окружения (если нужно): + +```bash +NON_INTERACTIVE=1 pm2 start server.js --name deepseek-api --update-env +``` + +Проверка: + +```bash +pm2 status +curl http://127.0.0.1:9655/health +``` + +### Конфиг ecosystem (рекомендуется) + +В репозитории есть готовый `ecosystem.config.cjs`: + +```bash +pm2 delete deepseek-api 2>/dev/null || true +pm2 start ecosystem.config.cjs +pm2 save +pm2 startup # автозапуск после перезагрузки сервера +``` + +### Логи + +```bash +pm2 logs deepseek-api # live-лог (Ctrl+C не останавливает процесс) +pm2 logs deepseek-api --lines 100 # последние 100 строк +pm2 flush deepseek-api # очистить логи +``` + +Файлы логов по умолчанию: + +```text +~/.pm2/logs/deepseek-api-out.log +~/.pm2/logs/deepseek-api-error.log +``` + +### Управление процессом + +```bash +pm2 restart deepseek-api +pm2 stop deepseek-api +pm2 delete deepseek-api +pm2 monit # CPU/RAM в терминале +``` + +После обновления auth (`npm run auth`) перезапустите proxy: `pm2 restart deepseek-api`. + +--- + ## ✅ Проверка работы ```bash diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs new file mode 100644 index 0000000..48c9de1 --- /dev/null +++ b/ecosystem.config.cjs @@ -0,0 +1,11 @@ +module.exports = { + apps: [{ + name: 'deepseek-api', + script: 'server.js', + cwd: __dirname, + env: { + NON_INTERACTIVE: '1', + PORT: '9655', + }, + }], +}; diff --git a/package.json b/package.json index 3e2e88c..7bbf1ab 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "auth": "node scripts/auth.js", "deepseek:auth": "node scripts/deepseek_chrome_auth.js", "client": "node client.js", - "test": "node --check server.js && node --check scripts/auth.js && node --check scripts/deepseek_chrome_auth.js && node --check scripts/probe_deepseek_models.js && node --check client.js && node --check scripts/live_agentic_smoke_tests.mjs", + "test": "node --check server.js && node --check scripts/auth.js && node --check scripts/deepseek_console_auth.js && node --check scripts/deepseek_chrome_auth.js && node --check scripts/probe_deepseek_models.js && node --check client.js && node --check scripts/live_agentic_smoke_tests.mjs", "test:live": "node scripts/live_agentic_smoke_tests.mjs" }, "keywords": [ diff --git a/scripts/auth.js b/scripts/auth.js index 5c802cf..cc8e7e6 100755 --- a/scripts/auth.js +++ b/scripts/auth.js @@ -13,6 +13,53 @@ 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 promptHidden(question) { + if (!process.stdin.isTTY) return prompt(question); + + return new Promise(resolve => { + const stdin = process.stdin; + const stdout = process.stdout; + let value = ''; + stdout.write(question); + + const wasRaw = stdin.isRaw; + stdin.setRawMode(true); + stdin.resume(); + stdin.setEncoding('utf8'); + + const cleanup = () => { + stdin.removeListener('data', onData); + stdin.setRawMode(wasRaw); + stdin.pause(); + }; + + const onData = (chunk) => { + for (const c of String(chunk)) { + if (c === '\n' || c === '\r') { + stdout.write('\n'); + cleanup(); + resolve(value); + return; + } + if (c === '\u0003') { + cleanup(); + process.exit(130); + } + if (c === '\b' || c === '\x7f') { + if (value.length > 0) { + value = value.slice(0, -1); + stdout.write('\b \b'); + } + continue; + } + value += c; + stdout.write('*'); + } + }; + + stdin.on('data', onData); + }); +} function divider() { console.log('======================================================'); } function watermark(prefix = 'ForgetMeAI') { return `${prefix}: ${WATERMARK}`; } function loadAuth() { @@ -39,6 +86,25 @@ function removeLocalAuth() { if (fs.existsSync(AUTH_PATH)) fs.rmSync(AUTH_PATH, { force: true }); console.log('Удалён deepseek-auth.json. Chrome profile оставлен, чтобы не разлогинивать браузер без нужды.'); } +async function runConsoleAuth() { + const login = (process.env.DEEPSEEK_LOGIN ?? '').trim() || (await prompt('DeepSeek логин (email/phone): ')).trim(); + const password = (process.env.DEEPSEEK_PASSWORD ?? '').trim() || (await promptHidden('DeepSeek пароль: ')).trim(); + + if (!login || !password) { + console.error('[auth] Нужны DEEPSEEK_LOGIN и DEEPSEEK_PASSWORD (логин и пароль).'); + process.exitCode = 2; + return; + } + + const script = path.join(__dirname, 'deepseek_console_auth.js'); + const env = { + ...process.env, + DEEPSEEK_LOGIN: login, + DEEPSEEK_PASSWORD: password, + }; + const result = spawnSync(process.execPath, [script], { stdio: 'inherit', env }); + process.exitCode = result.status === 0 ? 0 : 2; +} function printHelp() { divider(); console.log('FreeDeepseekAPI — управление DeepSeek Web login'); @@ -46,6 +112,7 @@ function printHelp() { divider(); console.log('Опции:'); console.log(' --login Открыть Chrome и обновить auth'); + console.log(' --login-console Ввести логин/пароль в консоли и обновить auth'); console.log(' --status Показать статус auth'); console.log(' --remove Удалить локальный deepseek-auth.json'); console.log(' --help Справка'); @@ -62,18 +129,21 @@ async function menu() { console.log('1 - Авторизоваться / обновить DeepSeek login'); console.log('2 - Показать статус'); console.log('3 - Удалить локальный auth файл'); - console.log('4 - Выход'); - const choice = (await prompt('Ваш выбор (Enter = 4): ')) || '4'; + console.log('4 - Авторизация в консоли (логин/пароль)'); + console.log('5 - Выход'); + const choice = (await prompt('Ваш выбор (Enter = 5): ')) || '5'; if (choice === '1') runDirectAuth(); else if (choice === '2') { status(); await prompt('\nНажмите Enter, чтобы вернуться в меню...'); } else if (choice === '3') removeLocalAuth(); - else if (choice === '4') break; + else if (choice === '4') await runConsoleAuth(); + 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('--login-console') || args.has('--console-login')) return void runConsoleAuth(); if (args.has('--status') || args.has('--list')) return status(); if (args.has('--remove')) return removeLocalAuth(); await menu(); diff --git a/scripts/deepseek_chrome_auth.js b/scripts/deepseek_chrome_auth.js index 4d90b9f..c507ffa 100755 --- a/scripts/deepseek_chrome_auth.js +++ b/scripts/deepseek_chrome_auth.js @@ -30,6 +30,9 @@ const outPath = process.env.DEEPSEEK_AUTH_PATH || path.join(repoRoot, 'deepseek- 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 || ''); +const consoleLogin = (process.env.DEEPSEEK_LOGIN || '').trim(); +const consolePassword = (process.env.DEEPSEEK_PASSWORD || '').trim(); +const autoLoginEnabled = /^(1|true|yes|on)$/i.test(process.env.DEEPSEEK_AUTO_LOGIN || '') || (!!consoleLogin && !!consolePassword); function shellPatternSafe(s) { return String(s).replace(/[\\"']/g, '.'); @@ -40,13 +43,13 @@ function sleepSync(ms) { } function killExistingTestingChrome() { - if (process.platform !== 'darwin') return; + if (process.platform !== 'darwin' && process.platform !== 'linux') return; const patterns = [ `--remote-debugging-port=${port}`, profileDir, ].map(shellPatternSafe); for (const pattern of patterns) { - try { execFileSync('/usr/bin/pkill', ['-f', pattern], { stdio: 'ignore' }); } catch {} + try { execFileSync('pkill', ['-f', pattern], { stdio: 'ignore' }); } catch {} } sleepSync(800); } @@ -70,11 +73,62 @@ function removeProfileSafely(dir) { } } +function platformChromeDefaults() { + if (process.platform === 'darwin') { + return [ + '/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing', + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + ]; + } + if (process.platform === 'linux') { + return [ + '/usr/bin/google-chrome-stable', + '/usr/bin/google-chrome', + '/usr/bin/chromium-browser', + '/usr/bin/chromium', + '/snap/bin/chromium', + ]; + } + if (process.platform === 'win32') { + const localAppData = process.env.LOCALAPPDATA || ''; + const programFiles = process.env.ProgramFiles || 'C:\\Program Files'; + const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)'; + return [ + path.join(programFiles, 'Google', 'Chrome', 'Application', 'chrome.exe'), + path.join(programFilesX86, 'Google', 'Chrome', 'Application', 'chrome.exe'), + path.join(localAppData, 'Google', 'Chrome', 'Application', 'chrome.exe'), + ]; + } + return []; +} + +function puppeteerCacheCandidates(cacheRoot) { + const candidates = []; + let versions = []; + try { versions = fs.readdirSync(cacheRoot); } catch { return candidates; } + + for (const version of versions) { + const base = path.join(cacheRoot, version); + if (process.platform === 'darwin') { + for (const sub of ['chrome-mac-arm64', 'chrome-mac-x64']) { + candidates.push(path.join( + base, sub, 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing' + )); + } + } else if (process.platform === 'linux') { + candidates.push(path.join(base, 'chrome-linux64', 'chrome')); + } else if (process.platform === 'win32') { + candidates.push(path.join(base, 'chrome-win64', 'chrome.exe')); + } + } + return candidates; +} + function resolveChromePath() { if (process.env.CHROME_PATH) return process.env.CHROME_PATH; // Match FreeQwenApi: prefer Puppeteer's bundled "Google Chrome for Testing" - // instead of the user's normal /Applications/Google Chrome.app. + // when puppeteer is installed locally or in a sibling repo. for (const base of [repoRoot, qwenRepoRoot]) { try { const puppeteerPath = require.resolve('puppeteer', { paths: [base] }); @@ -86,17 +140,20 @@ function resolveChromePath() { } catch {} } - const cacheRoot = path.join(process.env.HOME || '', '.cache', 'puppeteer', 'chrome'); - try { - const candidates = fs.readdirSync(cacheRoot) - .map(dir => path.join(cacheRoot, dir, 'chrome-mac-arm64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing')) - .filter(p => fs.existsSync(p)) - .sort() - .reverse(); - if (candidates[0]) return candidates[0]; - } catch {} - - return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; + const home = process.env.HOME || process.env.USERPROFILE || ''; + const cacheRoot = path.join(home, '.cache', 'puppeteer', 'chrome'); + const fromCache = puppeteerCacheCandidates(cacheRoot) + .filter(p => fs.existsSync(p)) + .sort() + .reverse(); + if (fromCache[0]) return fromCache[0]; + + for (const p of platformChromeDefaults()) { + if (fs.existsSync(p)) return p; + } + + const defaults = platformChromeDefaults(); + return defaults[0] || 'google-chrome'; } const chromePath = resolveChromePath(); @@ -106,6 +163,156 @@ function ask(q) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise(resolve => rl.question(q, ans => { rl.close(); resolve(ans); })); } + +async function autoLoginWithCredentials(cdp, login, password) { + // Best-effort "fill & submit" using DOM heuristics. + // If DeepSeek uses SSO/captcha, this may fail and user will need to complete login manually. + const loginJson = JSON.stringify(String(login)); + const passwordJson = JSON.stringify(String(password)); + const expression = `(async () => { + const loginValue = ${loginJson}; + const passwordValue = ${passwordJson}; + + const isVisible = (el) => { + if (!el) return false; + const r = el.getBoundingClientRect && el.getBoundingClientRect(); + if (!r) return false; + const style = window.getComputedStyle ? window.getComputedStyle(el) : null; + const disp = style ? style.display : ''; + const vis = style ? style.visibility : ''; + return r.width > 0 && r.height > 0 && disp !== 'none' && vis !== 'hidden'; + }; + + const norm = (s) => String(s || '').toLowerCase().trim(); + const inputKeywords = { + login: ['email', 'e-mail', 'login', 'username', 'phone', 'телефон', 'почта', 'user', 'e-mail'], + password: ['password', 'пароль', 'pass', 'pwd'] + }; + + const scoreInput = (el, kind) => { + const type = norm(el.type); + const name = norm(el.getAttribute && el.getAttribute('name')); + const id = norm(el.getAttribute && el.getAttribute('id')); + const placeholder = norm(el.getAttribute && el.getAttribute('placeholder')); + const aria = norm(el.getAttribute && (el.getAttribute('aria-label') || el.getAttribute('aria-labelledby'))); + const autocomplete = norm(el.getAttribute && el.getAttribute('autocomplete')); + const blob = [type, name, id, placeholder, aria, autocomplete].filter(Boolean).join(' '); + const kws = inputKeywords[kind] || []; + let score = 0; + for (const kw of kws) if (blob.includes(kw)) score += 2; + // Strong hints + if (kind === 'password' && type === 'password') score += 10; + if (kind === 'login' && (type === 'email' || type === 'tel')) score += 8; + if (kind === 'login' && (autocomplete.includes('username') || autocomplete.includes('email'))) score += 6; + return score; + }; + + const visibleInputs = Array.from(document.querySelectorAll('input')).filter(isVisible); + let loginInput = null; + let passwordInput = null; + + const pickBest = (kind) => { + let best = null; + let bestScore = -1; + for (const el of visibleInputs) { + const s = scoreInput(el, kind); + if (s > bestScore) { bestScore = s; best = el; } + } + // Require at least some hint unless there is an exact match for password. + if (kind === 'password') return bestScore >= 5 ? best : null; + return bestScore >= 3 ? best : null; + }; + + loginInput = pickBest('login'); + passwordInput = pickBest('password'); + + // If we are not on the login form yet, try to open it by clicking "Log in"/"Войти"/"Sign in". + const clickLoginButton = async () => { + const buttons = Array.from(document.querySelectorAll('button, a, input[type="submit"], input[type="button"]')).filter(isVisible); + const btnKeywords = ['log in', 'sign in', 'login', 'войти', 'продолжить', 'continue', 'next']; + for (const b of buttons) { + const text = norm(b.innerText || b.value || b.getAttribute('aria-label') || b.getAttribute('title')); + if (btnKeywords.some(k => text.includes(k))) { + b.click(); + await new Promise(r => setTimeout(r, 1200)); + return true; + } + } + return false; + }; + + if ((!loginInput || !passwordInput) && loginValue && passwordValue) { + await clickLoginButton(); + } + + // Recompute after possible navigation/modal. + const visibleInputs2 = Array.from(document.querySelectorAll('input')).filter(isVisible); + const visibleInputsRef = visibleInputs2.length ? visibleInputs2 : visibleInputs; + const pickBest2 = (kind) => { + let best = null; + let bestScore = -1; + for (const el of visibleInputsRef) { + const s = scoreInput(el, kind); + if (s > bestScore) { bestScore = s; best = el; } + } + if (kind === 'password') return bestScore >= 5 ? best : null; + return bestScore >= 3 ? best : null; + }; + loginInput = pickBest2('login'); + passwordInput = pickBest2('password'); + + const fill = (el, val) => { + if (!el) return false; + el.focus && el.focus(); + el.value = val; + el.dispatchEvent(new Event('input', { bubbles: true })); + el.dispatchEvent(new Event('change', { bubbles: true })); + return true; + }; + + const didLoginFill = fill(loginInput, loginValue); + const didPasswordFill = fill(passwordInput, passwordValue); + + const trySubmit = () => { + const submitKeywords = ['log in', 'sign in', 'login', 'войти', 'продолжить', 'continue', 'next']; + const submitters = Array.from(document.querySelectorAll('button, input[type="submit"], input[type="button"]')).filter(isVisible); + for (const s of submitters) { + const text = norm(s.innerText || s.value || s.getAttribute('aria-label') || s.getAttribute('title')); + if (submitKeywords.some(k => text.includes(k))) { + s.click(); + return true; + } + } + const form = (passwordInput && passwordInput.form) || (loginInput && loginInput.form); + if (form) { + try { + form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); + form.submit(); + return true; + } catch {} + } + return false; + }; + + const submitClicked = trySubmit(); + return { + loginFound: !!loginInput, + passwordFound: !!passwordInput, + didLoginFill, + didPasswordFill, + submitClicked, + locationHref: String(location.href || '') + }; + })()`; + + const evalRes = await cdp.send('Runtime.evaluate', { + expression, + returnByValue: true, + awaitPromise: true, + }); + return evalRes.result && evalRes.result.value ? evalRes.result.value : {}; +} + async function fetchJson(u, opts) { const r = await fetch(u, opts); if (!r.ok) throw new Error(`${u} -> HTTP ${r.status}`); @@ -235,18 +442,22 @@ async function main() { } else { console.log(`[auth] Starting clean Chrome for Testing profile: ${profileDir}`); console.log(`[auth] Browser executable: ${chromePath}`); - const chrome = spawn(chromePath, [ + const chromeArgs = [ `--user-data-dir=${profileDir}`, `--remote-debugging-port=${port}`, - '--use-mock-keychain', '--password-store=basic', '--disable-sync', '--disable-extensions', '--disable-component-extensions-with-background-pages', '--disable-features=AutofillServerCommunication,OptimizationHints,MediaRouter,InterestFeedContentSuggestions,Translate', '--no-first-run', '--no-default-browser-check', '--disable-infobars', - url, - ], { stdio: 'ignore', detached: true }); + ]; + if (process.platform === 'darwin') chromeArgs.push('--use-mock-keychain'); + if (process.platform === 'linux') { + chromeArgs.push('--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'); + } + chromeArgs.push(url); + const chrome = spawn(chromePath, chromeArgs, { stdio: 'ignore', detached: true }); chrome.unref(); } @@ -257,16 +468,43 @@ async function main() { await cdp.send('Runtime.enable'); await cdp.send('Network.enable'); - console.log('\n[auth] Chrome открыт. Войди в DeepSeek в ЭТОМ отдельном окне.'); - console.log('[auth] После логина отправь в DeepSeek короткое сообщение, например: ok'); - await ask('[auth] Когда залогинился и отправил тестовое сообщение — нажми ENTER здесь: '); + console.log('\n[auth] Chrome открыт в браузере для входа в DeepSeek.'); + const canAutoLogin = autoLoginEnabled && consoleLogin && consolePassword; + if (canAutoLogin) { + console.log('[auth] Автовход включен: пытаемся заполнить логин/пароль из консоли...'); + try { + const autoRes = await autoLoginWithCredentials(cdp, consoleLogin, consolePassword); + console.log(`[auth] Auto-fill: login=${autoRes.loginFound ? 'OK' : 'MISS'}, password=${autoRes.passwordFound ? 'OK' : 'MISS'}, submit=${autoRes.submitClicked ? 'OK' : 'MISS'}`); + if (autoRes.locationHref) console.log(`[auth] page: ${autoRes.locationHref}`); + } catch (e) { + console.log('[auth] Auto-login attempt failed: ' + e.message); + } + } else { + console.log('[auth] Войди в DeepSeek в ЭТОМ отдельном окне.'); + console.log('[auth] После логина отправь в DeepSeek короткое сообщение, например: ok'); + await ask('[auth] Когда залогинился и отправил тестовое сообщение — нажми ENTER здесь: '); + } let auth = null; - for (let i = 0; i < 20; i++) { + const preAttempts = canAutoLogin ? 40 : 20; + for (let i = 0; i < preAttempts; i++) { auth = await readPageAuth(cdp); if (auth.token && auth.cookie) break; await sleep(500); } + + if ((!auth || !auth.token || !auth.cookie) && canAutoLogin) { + console.log('[auth] token/cookie не появились после автозаполнения.'); + console.log('[auth] Возможно нужна ручная проверка (captcha/2FA). Заверши вход в окне Chrome и нажми ENTER здесь:'); + await ask('[auth] Продолжить получение auth (после завершения логина) — нажми ENTER: '); + for (let i = 0; i < 20; i++) { + auth = await readPageAuth(cdp); + if (auth.token && auth.cookie) break; + await sleep(500); + } + } + + auth = auth || await readPageAuth(cdp); const { href, cookiesCount, ...persisted } = auth; fs.writeFileSync(outPath, JSON.stringify(persisted, null, 2)); console.log(`[auth] Saved: ${outPath}`); diff --git a/scripts/deepseek_console_auth.js b/scripts/deepseek_console_auth.js new file mode 100644 index 0000000..b3aaf18 --- /dev/null +++ b/scripts/deepseek_console_auth.js @@ -0,0 +1,192 @@ +#!/usr/bin/env node +/* + Console login for DeepSeek Web without Chrome. + + Uses the same HTTP login endpoint as the official mobile client: + POST https://chat.deepseek.com/api/v0/users/login + + Usage: + DEEPSEEK_LOGIN=email DEEPSEEK_PASSWORD=secret node scripts/deepseek_console_auth.js +*/ +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); + +const repoRoot = path.resolve(__dirname, '..'); +const outPath = process.env.DEEPSEEK_AUTH_PATH || path.join(repoRoot, 'deepseek-auth.json'); +const DEFAULT_WASM_URL = process.env.DEEPSEEK_DEFAULT_WASM_URL || 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm'; +const BASE_URL = 'https://chat.deepseek.com'; + +class CookieJar { + constructor() { this.map = new Map(); } + ingest(response) { + const cookies = typeof response.headers.getSetCookie === 'function' + ? response.headers.getSetCookie() + : []; + for (const line of cookies) { + const part = String(line).split(';')[0]; + const eq = part.indexOf('='); + if (eq > 0) this.map.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim()); + } + } + set(name, value) { if (name && value != null) this.map.set(name, String(value)); } + toString() { return [...this.map.entries()].map(([k, v]) => `${k}=${v}`).join('; '); } +} + +function webHeaders(cookie = '', extra = {}) { + 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', + 'x-client-version': '2.0.0', + 'x-client-locale': 'ru', + 'x-client-timezone-offset': String(-new Date().getTimezoneOffset()), + 'x-app-version': '2.0.0', + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Origin': BASE_URL, + 'Referer': `${BASE_URL}/`, + ...(cookie ? { Cookie: cookie } : {}), + ...extra, + }; +} + +function iosLoginHeaders() { + return { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': 'DeepSeek/2 CFNetwork/1568.100.1 Darwin/24.0.0', + 'x-client-platform': 'ios', + 'x-client-version': '2.0.4', + 'x-client-bundle-id': 'com.deepseek.chat', + 'x-client-locale': 'en_US', + 'x-client-timezone-offset': String(-new Date().getTimezoneOffset()), + 'x-rangers-id': String(Math.floor(Math.random() * 1e18)), + }; +} + +async function readJson(response, label) { + const text = await response.text(); + let data; + try { data = JSON.parse(text); } + catch { throw new Error(`${label}: non-JSON response (${response.status}): ${text.slice(0, 200)}`); } + return data; +} + +function assertLoginOk(data, label) { + if (data.code !== 0) { + throw new Error(`${label}: ${data.msg || `API code ${data.code}`}`); + } + const nested = data.data || {}; + const bizCode = nested.biz_code ?? 0; + const bizMsg = nested.biz_msg || ''; + if (bizCode !== 0) { + throw new Error(`${label}: ${bizMsg || `biz_code ${bizCode}`}`); + } + const token = nested.biz_data?.user?.token; + if (!token) throw new Error(`${label}: token missing in response`); + return token; +} + +async function loginRequest(email, password, jar, variant) { + const deviceId = crypto.randomUUID(); + const headers = variant === 'ios' + ? { ...iosLoginHeaders(), ...(jar.toString() ? { Cookie: jar.toString() } : {}) } + : webHeaders(jar.toString()); + + const response = await fetch(`${BASE_URL}/api/v0/users/login`, { + method: 'POST', + headers, + body: JSON.stringify({ + email, + password, + device_id: deviceId, + os: variant === 'ios' ? 'ios' : 'web', + }), + }); + jar.ingest(response); + const data = await readJson(response, `login (${variant})`); + const token = assertLoginOk(data, `login (${variant})`); + jar.set('token', token); + return { token, user: data.data?.biz_data?.user || {} }; +} + +async function warmupWebSession(token, jar) { + const authHeaders = webHeaders(jar.toString(), { Authorization: `Bearer ${token}` }); + + const powResp = await fetch(`${BASE_URL}/api/v0/chat/create_pow_challenge`, { + method: 'POST', + headers: authHeaders, + body: JSON.stringify({ target_path: '/api/v0/chat/completion' }), + }); + jar.ingest(powResp); + const powData = await readJson(powResp, 'pow challenge'); + if (powData.code !== 0) { + throw new Error(`Session check failed: ${powData.msg || `API code ${powData.code}`}`); + } + + const sessResp = await fetch(`${BASE_URL}/api/v0/chat_session/create`, { + method: 'POST', + headers: authHeaders, + body: '{}', + }); + jar.ingest(sessResp); + const sessData = await readJson(sessResp, 'chat session'); + if (sessData.code !== 0) { + throw new Error(`Session create failed: ${sessData.msg || `API code ${sessData.code}`}`); + } +} + +function buildCookieString(jar, token) { + let cookie = jar.toString(); + if (!cookie) cookie = `token=${token}`; + else if (!cookie.includes('token=')) cookie = `token=${token}; ${cookie}`; + return cookie; +} + +async function main() { + const email = (process.env.DEEPSEEK_LOGIN || '').trim(); + const password = (process.env.DEEPSEEK_PASSWORD || '').trim(); + if (!email || !password) { + throw new Error('DEEPSEEK_LOGIN and DEEPSEEK_PASSWORD are required'); + } + + console.log('[auth] HTTP login (без Chrome)...'); + const jar = new CookieJar(); + let token = ''; + let lastError = null; + + for (const variant of ['web', 'ios']) { + try { + const result = await loginRequest(email, password, jar, variant); + token = result.token; + console.log(`[auth] Login OK (${variant})`); + break; + } catch (e) { + lastError = e; + console.log(`[auth] Login via ${variant} failed: ${e.message}`); + } + } + if (!token) throw lastError || new Error('Login failed'); + + console.log('[auth] Проверка web-сессии...'); + await warmupWebSession(token, jar); + + const auth = { + token, + cookie: buildCookieString(jar, token), + hif_dliq: '', + hif_leim: '', + wasmUrl: DEFAULT_WASM_URL, + baseUrl: BASE_URL, + }; + + fs.writeFileSync(outPath, JSON.stringify(auth, null, 2)); + console.log(`[auth] Saved: ${outPath}`); + console.log(`[auth] token: OK (${auth.token.length} chars)`); + console.log(`[auth] cookie: OK (${auth.cookie.length} chars)`); +} + +main().catch(e => { + console.error('[auth] ERROR:', e.message || e); + process.exit(1); +}); diff --git a/server.js b/server.js index 190f740..2c71b0f 100755 --- a/server.js +++ b/server.js @@ -51,6 +51,11 @@ function prompt(question) { return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans); })); } function isTruthy(value) { return typeof value === 'string' && ['1','true','yes','on'].includes(value.trim().toLowerCase()); } +function shouldSkipStartupMenu() { + if (isTruthy(process.env.SKIP_ACCOUNT_MENU) || isTruthy(process.env.NON_INTERACTIVE)) return true; + // pm2/systemd/nohup: no interactive terminal attached + return !process.stdin.isTTY; +} // === Per-Agent Session Store === const sessions = new Map(); // keyed by agent ID (from `user` field) @@ -1337,7 +1342,7 @@ function printStatus() { } async function showStartupMenu() { - if (isTruthy(process.env.SKIP_ACCOUNT_MENU) || isTruthy(process.env.NON_INTERACTIVE)) { + if (shouldSkipStartupMenu()) { if (!hasAuthConfig()) loadDeepSeekConfig({ fatal: true }); return true; }