diff --git a/README.md b/README.md index 1329785..99c0b04 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ ForgetMeAI: https://t.me/forgetmeai - [Diagnostics / doctor](#-diagnostics--doctor) - [Session reuse и сброс чатов](#-session-reuse-и-сброс-чатов) - [Multi-account pool](#-multi-account-pool) -- [Идеи для консольной авторизации](#-идеи-для-консольной-авторизации) +- [Консольная авторизация](#-консольная-авторизация) - [Проверка работы](#-проверка-работы) - [Примеры запросов](#-примеры-запросов) - [Chat Completions](#chat-completions) @@ -401,18 +401,23 @@ DEEPSEEK_ACCOUNT_COOLDOWN_MS=600000 npm start --- -## 🔑 Идеи для консольной авторизации +## 🔑 Консольная авторизация -Парольный flow из PR #3 можно делать, но безопаснее не хранить пароль и не делать это дефолтом. Нормальная реализация: +Если Chrome недоступен, авторизацию можно выполнить напрямую через HTTP API DeepSeek: -1. `npm run auth:console` спрашивает email/телефон и пароль через hidden prompt. -2. Пароль держится только в памяти процесса, не пишется в файлы/logs/history. -3. Скрипт повторяет Web login flow через `fetch`/CDP: получает captcha/verify challenge, отдаёт человеку ссылку/код, ждёт подтверждение. -4. После успешного login сохраняется только `deepseek-auth.json` стандартного формата. -5. Если DeepSeek просит captcha/2FA — скрипт честно говорит “открой ссылку, пройди проверку, нажми Enter”, а не пытается обходить защиту. -6. Для VPS лучше режим `auth:console --no-save-password --output deepseek-auth.json`. +```bash +npm run auth -- --login-console +``` + +Команда запросит логин и пароль в терминале; пароль вводится скрыто. Для автоматизированного запуска можно использовать переменные окружения: + +```bash +DEEPSEEK_LOGIN="email@example.com" DEEPSEEK_PASSWORD="your-password" npm run auth -- --login-console +``` + +После успешного входа скрипт проверяет web-сессию и сохраняет токен и cookies в `deepseek-auth.json`. При необходимости captcha или 2FA DeepSeek может отклонить HTTP-вход — в таком случае используйте браузерный режим `npm run auth -- --login`. -Минимальный безопасный MVP: console auth только интерактивный, без env-пароля. Допустимый automation-вариант: `DEEPSEEK_EMAIL=... npm run auth:console`, но пароль всё равно вводится hidden prompt. +> ⚠️ Не храните логин и пароль в общих скриптах, истории shell или CI-логах. Файл `deepseek-auth.json` содержит действующие credentials и не должен попадать в Git. --- 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 aedbae7..2e93486 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "doctor": "node scripts/doctor.js", "deepseek:auth": "node scripts/deepseek_chrome_auth.js", "client": "node client.js", - "test": "node --check server.js && node --check lib/pow.js && node --check scripts/auth.js && node --check scripts/auth_import.js && node --check scripts/doctor.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 && node --test tests/unit.test.js", + "test": "node --check server.js && node --check lib/pow.js && node --check scripts/auth.js && node --check scripts/auth_import.js && node --check scripts/doctor.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 && node --test tests/unit.test.js", "test:live": "node scripts/live_agentic_smoke_tests.mjs" }, "keywords": [ diff --git a/scripts/auth.js b/scripts/auth.js index 6ee6d10..902cf49 100755 --- a/scripts/auth.js +++ b/scripts/auth.js @@ -13,18 +13,42 @@ 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) { 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() { - try { return JSON.parse(fs.readFileSync(AUTH_PATH, 'utf8')); } - catch { return null; } -} +function loadAuth() { try { return JSON.parse(fs.readFileSync(AUTH_PATH, 'utf8')); } catch { return null; } } function status() { const auth = loadAuth(); console.log('\nDeepSeek аккаунт:'); - if (!auth) { - console.log(' ❌ deepseek-auth.json не найден'); - } else { + if (!auth) console.log(' ❌ deepseek-auth.json не найден'); + 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'}`); @@ -32,17 +56,24 @@ function status() { } } function runDirectAuth() { - const script = path.join(__dirname, 'deepseek_chrome_auth.js'); - return spawnSync(process.execPath, [script], { stdio: 'inherit', env: process.env }).status === 0; + return spawnSync(process.execPath, [path.join(__dirname, 'deepseek_chrome_auth.js')], { stdio: 'inherit', env: process.env }).status === 0; } function runImportAuth() { - const script = path.join(__dirname, 'auth_import.js'); - return spawnSync(process.execPath, [script], { stdio: 'inherit', env: process.env }).status === 0; + return spawnSync(process.execPath, [path.join(__dirname, 'auth_import.js')], { stdio: 'inherit', env: process.env }).status === 0; } 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 result = spawnSync(process.execPath, [path.join(__dirname, 'deepseek_console_auth.js')], { + stdio: 'inherit', env: { ...process.env, DEEPSEEK_LOGIN: login, DEEPSEEK_PASSWORD: password }, + }); + process.exitCode = result.status === 0 ? 0 : 2; +} function printHelp() { divider(); console.log('FreeDeepseekAPI — управление DeepSeek Web login'); @@ -50,6 +81,7 @@ function printHelp() { divider(); console.log('Опции:'); console.log(' --login Открыть Chrome и обновить auth'); + console.log(' --login-console Ввести логин/пароль в консоли и обновить auth'); console.log(' --import Импортировать готовый deepseek-auth.json / browser cookies'); console.log(' --status Показать статус auth'); console.log(' --remove Удалить локальный deepseek-auth.json'); @@ -59,28 +91,28 @@ function printHelp() { } async function menu() { while (true) { - divider(); - console.log(watermark()); - status(); - divider(); + divider(); console.log(watermark()); status(); divider(); console.log('Меню:'); console.log('1 - Авторизоваться / обновить DeepSeek login'); console.log('2 - Импортировать auth-файл / cookies'); console.log('3 - Показать статус'); - console.log('4 - Удалить локальный auth файл'); - console.log('5 - Выход'); - const choice = (await prompt('Ваш выбор (Enter = 5): ')) || '5'; + console.log('4 - Авторизация в консоли (логин/пароль)'); + console.log('5 - Удалить локальный auth файл'); + console.log('6 - Выход'); + const choice = (await prompt('Ваш выбор (Enter = 6): ')) || '6'; if (choice === '1') runDirectAuth(); else if (choice === '2') runImportAuth(); else if (choice === '3') { status(); await prompt('\nНажмите Enter, чтобы вернуться в меню...'); } - else if (choice === '4') removeLocalAuth(); - else if (choice === '5') break; + else if (choice === '4') await runConsoleAuth(); + else if (choice === '5') removeLocalAuth(); + else if (choice === '6') 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('--import')) return void runImportAuth(); if (args.has('--status') || args.has('--list')) return status(); if (args.has('--remove')) return removeLocalAuth(); diff --git a/scripts/deepseek_chrome_auth.js b/scripts/deepseek_chrome_auth.js index 91384fb..772936d 100755 --- a/scripts/deepseek_chrome_auth.js +++ b/scripts/deepseek_chrome_auth.js @@ -23,368 +23,410 @@ const readline = require('readline'); const repoRoot = path.resolve(__dirname, '..'); const qwenRepoRoot = path.resolve(repoRoot, '..', 'FreeQwenApi'); -const profileDir = - process.env.DEEPSEEK_CHROME_PROFILE || - path.join(repoRoot, '.chrome-for-testing-profile-deepseek'); +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'); +const outPath = process.env.DEEPSEEK_AUTH_PATH || 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 || '', -); +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, '.'); + return String(s).replace(/[\\"']/g, '.'); } function sleepSync(ms) { - try { - Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); - } catch {} + try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch {} } function killExistingTestingChrome() { - if (process.platform !== 'darwin') 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 {} - } - sleepSync(800); + if (process.platform !== 'darwin' && process.platform !== 'linux') return; + const patterns = [ + `--remote-debugging-port=${port}`, + profileDir, + ].map(shellPatternSafe); + for (const pattern of patterns) { + try { execFileSync('pkill', ['-f', pattern], { stdio: 'ignore' }); } catch {} + } + sleepSync(800); } function removeProfileSafely(dir) { - if (!fs.existsSync(dir)) return; - for (let i = 0; i < 5; i++) { - try { - fs.rmSync(dir, { - recursive: true, - force: true, - maxRetries: 5, - retryDelay: 250, - }); - if (!fs.existsSync(dir)) return; - } catch (e) { - if (i === 4) { - const staleDir = `${dir}.stale-${Date.now()}`; - fs.renameSync(dir, staleDir); - try { - fs.rmSync(staleDir, { - recursive: true, - force: true, - maxRetries: 3, - retryDelay: 250, - }); - } catch {} - console.log( - `[auth] Old profile was busy; moved it aside: ${staleDir}`, - ); - return; - } - } - sleepSync(300); + if (!fs.existsSync(dir)) return; + for (let i = 0; i < 5; i++) { + try { + fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 250 }); + if (!fs.existsSync(dir)) return; + } catch (e) { + if (i === 4) { + const staleDir = `${dir}.stale-${Date.now()}`; + fs.renameSync(dir, staleDir); + try { fs.rmSync(staleDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 250 }); } catch {} + console.log(`[auth] Old profile was busy; moved it aside: ${staleDir}`); + return; + } } + sleepSync(300); + } } -function resolveChromePath() { - if (process.env.CHROME_PATH) return process.env.CHROME_PATH; +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 []; +} - // Prefer Puppeteer's bundled "Google Chrome for Testing" when available. - for (const base of [repoRoot, qwenRepoRoot]) { - try { - const puppeteerPath = require.resolve('puppeteer', { - paths: [base], - }); - const puppeteer = require(puppeteerPath); - if (typeof puppeteer.executablePath === 'function') { - const p = puppeteer.executablePath(); - if (p && fs.existsSync(p)) return p; - } - } catch {} - } +function puppeteerCacheCandidates(cacheRoot) { + const candidates = []; + let versions = []; + try { versions = fs.readdirSync(cacheRoot); } catch { return candidates; } - // Try to locate Chrome for Testing in common Puppeteer cache locations. - // (The previous version was macOS-only, which broke Windows.) - const home = process.env.HOME || process.env.USERPROFILE || ''; - if (home) { - const cacheRoot = path.join(home, '.cache', 'puppeteer', 'chrome'); - try { - // macOS / Linux-style cache layout. - const candidates = fs - .readdirSync(cacheRoot) - .flatMap((dir) => { - const baseDir = path.join(cacheRoot, dir); - if (process.platform === 'darwin') { - return [ - path.join( - baseDir, - 'chrome-mac-arm64', - 'Google Chrome for Testing.app', - 'Contents', - 'MacOS', - 'Google Chrome for Testing', - ), - path.join( - baseDir, - 'chrome-mac-x64', - 'Google Chrome for Testing.app', - 'Contents', - 'MacOS', - 'Google Chrome for Testing', - ), - ]; - } - if (process.platform === 'win32') { - // On Windows, Puppeteer cache layouts are not always identical; try the most common one. - // Also consider that executable might be chrome.exe or chrome-win64\chrome.exe. - return [ - path.join(baseDir, 'chrome-win64', 'chrome.exe'), - path.join(baseDir, 'chrome-win64', 'chrome.exe'), - ]; - } - // linux - return [ - path.join(baseDir, 'chrome-linux64', 'chrome'), - path.join(baseDir, 'chrome-linux64', 'chrome.exe'), - ]; - }) - .filter((p) => p && fs.existsSync(p)) - .sort() - .reverse(); - if (candidates[0]) return candidates[0]; - } catch {} + 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; +} - // Last resort: OS-default Chrome locations. - // OS-default Chrome locations (keep it flexible and short). - if (process.platform === 'win32') { - const candidates = [ - 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', - 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', - ]; - for (const c of candidates) { - if (fs.existsSync(c)) return c; - } - } else if (process.platform === 'darwin') { - const candidates = [ - '/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing', - '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', - ]; - for (const c of candidates) { - if (fs.existsSync(c)) return c; - } - } +function resolveChromePath() { + if (process.env.CHROME_PATH) return process.env.CHROME_PATH; - // Final fallback: try legacy macOS Chrome path for backward compatibility - // (harmless on Windows because fs.existsSync above will fail). - const legacyMac = - '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; - if (fs.existsSync(legacyMac)) return legacyMac; + // Match FreeQwenApi: prefer Puppeteer's bundled "Google Chrome for Testing" + // when puppeteer is installed locally or in a sibling repo. + for (const base of [repoRoot, qwenRepoRoot]) { + try { + const puppeteerPath = require.resolve('puppeteer', { paths: [base] }); + const puppeteer = require(puppeteerPath); + if (typeof puppeteer.executablePath === 'function') { + const p = puppeteer.executablePath(); + if (p && fs.existsSync(p)) return p; + } + } catch {} + } + + 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]; - return ''; // handled by the caller with a better error message. + for (const p of platformChromeDefaults()) { + if (fs.existsSync(p)) return p; + } + + const defaults = platformChromeDefaults(); + return defaults[0] || 'google-chrome'; } const chromePath = resolveChromePath(); -function sleep(ms) { - return new Promise((r) => setTimeout(r, ms)); -} +function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } 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); - }), - ); + 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}`); - return await r.json(); + const r = await fetch(u, opts); + if (!r.ok) throw new Error(`${u} -> HTTP ${r.status}`); + return await r.json(); } async function devtoolsReady() { - try { - return await fetchJson(`http://127.0.0.1:${port}/json/version`); - } catch { - return null; - } + try { return await fetchJson(`http://127.0.0.1:${port}/json/version`); } + catch { return null; } } async function waitDevtools() { - for (let i = 0; i < 80; i++) { - const v = await devtoolsReady(); - if (v) return v; - await sleep(250); - } - throw new Error('Chrome DevTools endpoint did not start'); + for (let i = 0; i < 80; i++) { + const v = await devtoolsReady(); + if (v) return v; + await sleep(250); + } + throw new Error('Chrome DevTools endpoint did not start'); } async function getPageTarget() { - for (let i = 0; i < 40; i++) { - const targets = await fetchJson(`http://127.0.0.1:${port}/json`); - const page = - targets.find( - (t) => t.type === 'page' && /chat\.deepseek\.com/.test(t.url), - ) || targets.find((t) => t.type === 'page'); - if (page?.webSocketDebuggerUrl) return page; - await sleep(250); - } - throw new Error('No Chrome page target found'); + for (let i = 0; i < 40; i++) { + const targets = await fetchJson(`http://127.0.0.1:${port}/json`); + const page = targets.find(t => t.type === 'page' && /chat\.deepseek\.com/.test(t.url)) || targets.find(t => t.type === 'page'); + if (page?.webSocketDebuggerUrl) return page; + await sleep(250); + } + throw new Error('No Chrome page target found'); } class CDP { - constructor(wsUrl) { - this.ws = new WebSocket(wsUrl); - this.id = 0; - this.pending = new Map(); - this.events = []; - this.ws.onmessage = (ev) => { - const msg = JSON.parse(ev.data); - if (msg.id && this.pending.has(msg.id)) { - const { resolve, reject } = this.pending.get(msg.id); - this.pending.delete(msg.id); - msg.error - ? reject(new Error(JSON.stringify(msg.error))) - : resolve(msg.result); - } else if (msg.method) { - this.events.push(msg); - if (this.events.length > 1000) this.events.shift(); - } - }; - } - ready() { - return new Promise((resolve, reject) => { - this.ws.onopen = resolve; - this.ws.onerror = reject; - }); - } - send(method, params = {}) { - const id = ++this.id; - this.ws.send(JSON.stringify({ id, method, params })); - return new Promise((resolve, reject) => - this.pending.set(id, { resolve, reject }), - ); - } - close() { - try { - this.ws.close(); - } catch {} - } + constructor(wsUrl) { + this.ws = new WebSocket(wsUrl); + this.id = 0; + this.pending = new Map(); + this.events = []; + this.ws.onmessage = ev => { + const msg = JSON.parse(ev.data); + if (msg.id && this.pending.has(msg.id)) { + const { resolve, reject } = this.pending.get(msg.id); + this.pending.delete(msg.id); + msg.error ? reject(new Error(JSON.stringify(msg.error))) : resolve(msg.result); + } else if (msg.method) { + this.events.push(msg); + if (this.events.length > 1000) this.events.shift(); + } + }; + } + ready() { return new Promise((resolve, reject) => { this.ws.onopen = resolve; this.ws.onerror = reject; }); } + send(method, params = {}) { + const id = ++this.id; + this.ws.send(JSON.stringify({ id, method, params })); + return new Promise((resolve, reject) => this.pending.set(id, { resolve, reject })); + } + close() { try { this.ws.close(); } catch {} } } function parseMaybeJson(s) { - if (!s) return null; - try { - return JSON.parse(s); - } catch { - return null; - } + if (!s) return null; + try { return JSON.parse(s); } catch { return null; } } function normalizeToken(raw) { - if (!raw) return ''; - const parsed = parseMaybeJson(raw); - if (parsed && typeof parsed === 'object') - return ( - parsed.value || - parsed.token || - parsed.access_token || - parsed.accessToken || - '' - ); - return String(raw).trim(); + if (!raw) return ''; + const parsed = parseMaybeJson(raw); + if (parsed && typeof parsed === 'object') return parsed.value || parsed.token || parsed.access_token || parsed.accessToken || ''; + return String(raw).trim(); } async function readPageAuth(cdp) { - const evalRes = await cdp.send('Runtime.evaluate', { - expression: `(() => { + const evalRes = await cdp.send('Runtime.evaluate', { + expression: `(() => { const out = {href: location.href, localStorage:{}, sessionStorage:{}, resources: []}; for (let i=0;i r.name).filter(n => /wasm|chat\\/completion|pow|chat_session/.test(n)).slice(-100); return out; })()`, - returnByValue: true, - }); - const pageState = evalRes.result.value || {}; - const stores = [ - pageState.localStorage || {}, - pageState.sessionStorage || {}, - ]; - let token = ''; - for (const store of stores) { - for (const key of [ - 'userToken', - 'token', - 'auth_token', - 'access_token', - 'accessToken', - ]) { - token = normalizeToken(store[key]); - if (token) break; - } - if (token) break; + returnByValue: true, + }); + const pageState = evalRes.result.value || {}; + const stores = [pageState.localStorage || {}, pageState.sessionStorage || {}]; + let token = ''; + for (const store of stores) { + for (const key of ['userToken','token','auth_token','access_token','accessToken']) { + token = normalizeToken(store[key]); + if (token) break; } - if (!token) { - for (const store of stores) { - for (const [k, v] of Object.entries(store)) { - if (/token/i.test(k)) { - token = normalizeToken(v); - if (token) break; - } - } - if (token) break; - } + if (token) break; + } + if (!token) { + for (const store of stores) { + for (const [k, v] of Object.entries(store)) { + if (/token/i.test(k)) { token = normalizeToken(v); if (token) break; } + } + if (token) break; } + } - const cookieRes = await cdp.send('Network.getAllCookies'); - const cookies = (cookieRes.cookies || []).filter((c) => - /deepseek\.com$/.test(c.domain), - ); - const cookie = cookies.map((c) => `${c.name}=${c.value}`).join('; '); - - let hif_dliq = '', - hif_leim = ''; - for (const ev of cdp.events) { - const headers = ev.params?.headers || ev.params?.request?.headers; - if (!headers) continue; - for (const [k, v] of Object.entries(headers)) { - const lk = k.toLowerCase(); - if (lk === 'x-hif-dliq') hif_dliq = String(v); - if (lk === 'x-hif-leim') hif_leim = String(v); - if ( - lk === 'authorization' && - !token && - /^Bearer\s+/i.test(String(v)) - ) - token = String(v).replace(/^Bearer\s+/i, ''); - } + const cookieRes = await cdp.send('Network.getAllCookies'); + const cookies = (cookieRes.cookies || []).filter(c => /deepseek\.com$/.test(c.domain)); + const cookie = cookies.map(c => `${c.name}=${c.value}`).join('; '); + + let hif_dliq = '', hif_leim = ''; + for (const ev of cdp.events) { + const headers = ev.params?.headers || ev.params?.request?.headers; + if (!headers) continue; + for (const [k, v] of Object.entries(headers)) { + const lk = k.toLowerCase(); + if (lk === 'x-hif-dliq') hif_dliq = String(v); + if (lk === 'x-hif-leim') hif_leim = String(v); + if (lk === 'authorization' && !token && /^Bearer\s+/i.test(String(v))) token = String(v).replace(/^Bearer\s+/i, ''); } + } - const wasmUrl = - (pageState.resources || []).find((u) => /sha3.*\.wasm/.test(u)) || - 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm'; - return { - token, - cookie, - hif_dliq, - hif_leim, - wasmUrl, - baseUrl: 'https://chat.deepseek.com', - href: pageState.href, - cookiesCount: cookies.length, - }; + const wasmUrl = (pageState.resources || []).find(u => /sha3.*\.wasm/.test(u)) || + 'https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.7b9ca65ddd.wasm'; + return { token, cookie, hif_dliq, hif_leim, wasmUrl, baseUrl: 'https://chat.deepseek.com', href: pageState.href, cookiesCount: cookies.length }; } function chromeInstallHelp(missingPath) { - return `Chrome/Chrome for Testing not found${missingPath ? `: ${missingPath}` : ''}. + return `Chrome/Chrome for Testing not found${missingPath ? `: ${missingPath}` : ''}. How to fix: Windows PowerShell: @@ -393,7 +435,6 @@ How to fix: macOS: CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" npm run auth - # or install Chrome for Testing / Google Chrome. Linux / Chromium: CHROME_PATH=$(which chromium) npm run auth @@ -403,88 +444,93 @@ If Chrome is installed elsewhere, set CHROME_PATH to the real executable path.`; } async function main() { - if (!fs.existsSync(chromePath)) - throw new Error(chromeInstallHelp(chromePath)); - - if (!reuseChrome) { - killExistingTestingChrome(); - if (!keepProfile && fs.existsSync(profileDir)) { - removeProfileSafely(profileDir); - console.log( - `[auth] Removed old Chrome for Testing profile: ${profileDir}`, - ); - } + if (!fs.existsSync(chromePath)) throw new Error(chromeInstallHelp(chromePath)); + + if (!reuseChrome) { + killExistingTestingChrome(); + if (!keepProfile && fs.existsSync(profileDir)) { + removeProfileSafely(profileDir); + console.log(`[auth] Removed old Chrome for Testing profile: ${profileDir}`); } - fs.mkdirSync(profileDir, { recursive: true }); - - if (reuseChrome && (await devtoolsReady())) { - console.log(`[auth] Reusing Chrome DevTools on port ${port}`); - } else { - console.log( - `[auth] Starting clean Chrome for Testing profile: ${profileDir}`, - ); - console.log(`[auth] Browser executable: ${chromePath}`); - const chrome = spawn( - chromePath, - [ - `--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 }, - ); - chrome.unref(); + } + fs.mkdirSync(profileDir, { recursive: true }); + + if (reuseChrome && await devtoolsReady()) { + console.log(`[auth] Reusing Chrome DevTools on port ${port}`); + } else { + console.log(`[auth] Starting clean Chrome for Testing profile: ${profileDir}`); + console.log(`[auth] Browser executable: ${chromePath}`); + const chromeArgs = [ + `--user-data-dir=${profileDir}`, + `--remote-debugging-port=${port}`, + '--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', + ]; + 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(); + } + + await waitDevtools(); + const target = await getPageTarget(); + const cdp = new CDP(target.webSocketDebuggerUrl); + await cdp.ready(); + await cdp.send('Runtime.enable'); + await cdp.send('Network.enable'); - await waitDevtools(); - const target = await getPageTarget(); - const cdp = new CDP(target.webSocketDebuggerUrl); - await cdp.ready(); - 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 здесь: ', - ); - - let auth = null; + 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; + 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 = await readPageAuth(cdp); + if (auth.token && auth.cookie) break; + await sleep(500); } - const { href, cookiesCount, ...persisted } = auth; - fs.writeFileSync(outPath, JSON.stringify(persisted, null, 2)); - console.log(`[auth] Saved: ${outPath}`); - console.log(`[auth] page: ${href || 'unknown'}`); - console.log( - `[auth] token: ${persisted.token ? 'OK (' + persisted.token.length + ' chars)' : 'MISSING'}`, - ); - console.log( - `[auth] cookie: ${persisted.cookie ? 'OK (' + cookiesCount + ' cookies)' : 'MISSING'}`, - ); - console.log( - `[auth] hif headers: ${persisted.hif_dliq || persisted.hif_leim ? 'captured' : 'not captured/optional'}`, - ); - cdp.close(); - if (!persisted.token || !persisted.cookie) process.exitCode = 2; + } + + auth = auth || await readPageAuth(cdp); + const { href, cookiesCount, ...persisted } = auth; + fs.writeFileSync(outPath, JSON.stringify(persisted, null, 2)); + console.log(`[auth] Saved: ${outPath}`); + console.log(`[auth] page: ${href || 'unknown'}`); + console.log(`[auth] token: ${persisted.token ? 'OK (' + persisted.token.length + ' chars)' : 'MISSING'}`); + console.log(`[auth] cookie: ${persisted.cookie ? 'OK (' + cookiesCount + ' cookies)' : 'MISSING'}`); + console.log(`[auth] hif headers: ${persisted.hif_dliq || persisted.hif_leim ? 'captured' : 'not captured/optional'}`); + cdp.close(); + if (!persisted.token || !persisted.cookie) process.exitCode = 2; } -main().catch((e) => { - console.error('[auth] ERROR:', e); - process.exit(1); -}); +main().catch(e => { console.error('[auth] ERROR:', e); process.exit(1); }); 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 0bad77a..52d086f 100755 --- a/server.js +++ b/server.js @@ -86,6 +86,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; +} function isProxyAuthorized(authorization, expectedKey = PROXY_API_KEY) { if (!expectedKey) return true; @@ -2350,7 +2355,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; }