diff --git a/.gitignore b/.gitignore index 2610c6d..151f8f9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ node_modules/ *.log auth.json deepseek-auth.json +# Aggregate account export holding live tokens/cookies — never commit. +deepseek-accounts.json +# Runtime-added auth files contain secrets — keep the dir, ignore its contents. +data/accounts/*.json .chrome-profile-deepseek/ .chrome-for-testing-profile-deepseek/ .chrome-for-testing-profile-deepseek.stale-*/ diff --git a/client.js b/client.js index 773f904..ceb502c 100755 --- a/client.js +++ b/client.js @@ -15,6 +15,8 @@ const fs = require('fs'); const path = require('path'); +const os = require('os'); +const { solvePOW } = require('./lib/pow'); // Load from env or config file const CONFIG = { @@ -57,28 +59,8 @@ const BASE_HEADERS = { 'Cookie': CONFIG.cookie, 'Content-Type': 'application/json', }; -async function solvePOW(challenge) { - const resp = await fetch(CONFIG.wasmUrl); - const wasmBytes = await resp.arrayBuffer(); - const mod = await WebAssembly.instantiate(wasmBytes, { wbg: {} }); - const e = mod.instance.exports; - const encoder = new TextEncoder(); - const prefix = challenge.salt + '_' + challenge.expire_at + '_'; - const cBytes = encoder.encode(challenge.challenge); - const pBytes = encoder.encode(prefix); - const cP = e.__wbindgen_export_0(cBytes.length, 1) >>> 0; - const pP = e.__wbindgen_export_0(pBytes.length, 1) >>> 0; - new Uint8Array(e.memory.buffer, cP, cBytes.length).set(cBytes); - new Uint8Array(e.memory.buffer, pP, pBytes.length).set(pBytes); - const sp = e.__wbindgen_add_to_stack_pointer(-16); - e.wasm_solve(sp, cP, cBytes.length, pP, pBytes.length, challenge.difficulty); - const dv = new DataView(e.memory.buffer); - const code = dv.getInt32(sp, true); - const ans = dv.getFloat64(sp + 8, true); - e.__wbindgen_add_to_stack_pointer(16); - if (code === 0 || !Number.isFinite(ans) || ans <= 0) throw new Error('POW solve failed'); - return Math.floor(ans); -} +// solvePOW() is imported from lib/pow (compiled-module cache + WASM-fetch timeout), +// shared with server.js. async function askDeepSeek(prompt, onChunk) { const chalResp = await fetch('https://chat.deepseek.com/api/v0/chat/create_pow_challenge', { @@ -87,7 +69,7 @@ async function askDeepSeek(prompt, onChunk) { }); const chalData = await chalResp.json(); const challenge = chalData.data.biz_data.challenge; - const answer = await solvePOW(challenge); + const answer = await solvePOW(challenge, CONFIG.wasmUrl); const sessResp = await fetch('https://chat.deepseek.com/api/v0/chat_session/create', { method: 'POST', headers: BASE_HEADERS, body: '{}' @@ -145,23 +127,28 @@ async function askDeepSeek(prompt, onChunk) { return fullResponse; } +function readStdin() { + // fd 0 works on Windows too (unlike '/dev/stdin'); empty if no pipe. + try { return fs.readFileSync(0, 'utf8').trim(); } catch { return ''; } +} + async function main() { - const prompt = process.argv.slice(2).join(' ') || fs.readFileSync('/dev/stdin', 'utf8').trim(); + const prompt = process.argv.slice(2).join(' ') || readStdin(); if (!prompt) { console.error('Usage: node client.js "your prompt here"'); process.exit(1); } let fullText = ''; - const response = await askDeepSeek(prompt, (chunk) => { + await askDeepSeek(prompt, (chunk) => { process.stdout.write(chunk); fullText += chunk; }); process.stdout.write('\n'); - const ts = Date.now(); - fs.writeFileSync(`/tmp/deepseek_response_${ts}.txt`, fullText.trim()); - console.error(`\n[*] Saved /tmp/deepseek_response_${ts}.txt`); + const outFile = path.join(os.tmpdir(), `deepseek_response_${Date.now()}.txt`); + fs.writeFileSync(outFile, fullText.trim()); + console.error(`\n[*] Saved ${outFile}`); } main().catch(e => { diff --git a/lib/pow.js b/lib/pow.js new file mode 100644 index 0000000..8b1c8e3 --- /dev/null +++ b/lib/pow.js @@ -0,0 +1,54 @@ +'use strict'; +// Proof-of-work solver for the DeepSeek Web API. +// +// The PoW WASM module is immutable per URL, so we compile it ONCE and cache the +// compiled WebAssembly.Module; each solve only spins up a fresh instance (clean +// linear memory). This removes a network download + recompile from every single +// completion (and every retry), and gives the WASM fetch a hard timeout so a +// stalled CDN can never hang a request forever. +// +// Shared by server.js and client.js (previously duplicated verbatim). + +const moduleCache = new Map(); // wasmUrl -> Promise + +async function loadModule(wasmUrl, { timeoutMs = 15000 } = {}) { + if (!wasmUrl) throw new Error('POW: missing wasmUrl'); + if (!moduleCache.has(wasmUrl)) { + const p = (async () => { + const resp = await fetch(wasmUrl, { signal: AbortSignal.timeout(timeoutMs) }); + if (!resp.ok) throw new Error(`POW: could not fetch WASM (HTTP ${resp.status})`); + const bytes = await resp.arrayBuffer(); + return WebAssembly.compile(bytes); + })(); + moduleCache.set(wasmUrl, p); + // Don't cache a failed download — let the next solve retry the fetch. + p.catch(() => moduleCache.delete(wasmUrl)); + } + return moduleCache.get(wasmUrl); +} + +async function solvePOW(challenge, wasmUrl, opts = {}) { + const module = await loadModule(wasmUrl, opts); + // Instantiating from a compiled Module returns the Instance directly + // (no { instance, module } wrapper, unlike the bytes form). + const instance = await WebAssembly.instantiate(module, { wbg: {} }); + const e = instance.exports; + const encoder = new TextEncoder(); + const prefix = challenge.salt + '_' + challenge.expire_at + '_'; + const cBytes = encoder.encode(challenge.challenge); + const pBytes = encoder.encode(prefix); + const cP = e.__wbindgen_export_0(cBytes.length, 1) >>> 0; + const pP = e.__wbindgen_export_0(pBytes.length, 1) >>> 0; + new Uint8Array(e.memory.buffer, cP, cBytes.length).set(cBytes); + new Uint8Array(e.memory.buffer, pP, pBytes.length).set(pBytes); + const sp = e.__wbindgen_add_to_stack_pointer(-16); + e.wasm_solve(sp, cP, cBytes.length, pP, pBytes.length, challenge.difficulty); + const dv = new DataView(e.memory.buffer); + const code = dv.getInt32(sp, true); + const ans = dv.getFloat64(sp + 8, true); + e.__wbindgen_add_to_stack_pointer(16); + if (code === 0 || !Number.isFinite(ans) || ans <= 0) throw new Error('POW failed'); + return Math.floor(ans); +} + +module.exports = { solvePOW, _moduleCache: moduleCache }; diff --git a/package.json b/package.json index dfa855d..aedbae7 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 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_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/server.js b/server.js index 442feef..e469c73 100755 --- a/server.js +++ b/server.js @@ -16,6 +16,14 @@ const os = require('os'); const path = require('path'); const readline = require('readline'); const { spawnSync } = require('child_process'); +const { solvePOW } = require('./lib/pow'); + +// Per-DeepSeek-request network timeout. Plain fetch() has NO default timeout, so a +// stalled upstream would hang the inbound request (and pin the account) forever. +const DS_FETCH_TIMEOUT_MS = Number(process.env.DEEPSEEK_FETCH_TIMEOUT_MS || 60000); +function dsFetch(url, options = {}, timeoutMs = DS_FETCH_TIMEOUT_MS) { + return fetch(url, { ...options, signal: options.signal || AbortSignal.timeout(timeoutMs) }); +} const SERVER_HOST = os.hostname(); // Dynamic hostname detection const SERVER_PUBLIC_IP = (() => { @@ -224,28 +232,8 @@ function getOrCreateAgentSession(agentId) { return sessions.get(agentId); } -async function solvePOW(challenge, config = DS_CONFIG) { - const resp = await fetch(config.wasmUrl); - const wasmBytes = await resp.arrayBuffer(); - const mod = await WebAssembly.instantiate(wasmBytes, { wbg: {} }); - const e = mod.instance.exports; - const encoder = new TextEncoder(); - const prefix = challenge.salt + '_' + challenge.expire_at + '_'; - const cBytes = encoder.encode(challenge.challenge); - const pBytes = encoder.encode(prefix); - const cP = e.__wbindgen_export_0(cBytes.length, 1) >>> 0; - const pP = e.__wbindgen_export_0(pBytes.length, 1) >>> 0; - new Uint8Array(e.memory.buffer, cP, cBytes.length).set(cBytes); - new Uint8Array(e.memory.buffer, pP, pBytes.length).set(pBytes); - const sp = e.__wbindgen_add_to_stack_pointer(-16); - e.wasm_solve(sp, cP, cBytes.length, pP, pBytes.length, challenge.difficulty); - const dv = new DataView(e.memory.buffer); - const code = dv.getInt32(sp, true); - const ans = dv.getFloat64(sp + 8, true); - e.__wbindgen_add_to_stack_pointer(16); - if (code === 0 || !Number.isFinite(ans) || ans <= 0) throw new Error('POW failed'); - return Math.floor(ans); -} +// solvePOW() lives in lib/pow (compiled-module cache + WASM-fetch timeout), +// shared with client.js. Called as solvePOW(challenge, wasmUrl). const MODEL_CONFIGS = { // DeepSeek Web real model_type: default / UI name: "Быстрый". @@ -421,7 +409,7 @@ async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default') { session.messageCount = 0; } - const cr = await fetch('https://chat.deepseek.com/api/v0/chat/create_pow_challenge', { + const cr = await dsFetch('https://chat.deepseek.com/api/v0/chat/create_pow_challenge', { method: 'POST', headers: dsHeaders, body: JSON.stringify({ target_path: '/api/v0/chat/completion' }) }); @@ -437,10 +425,10 @@ async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default') { if (!challenge) { throw new Error('DeepSeek PoW response has no data.biz_data.challenge. Auth may be expired, captcha may be required, or DeepSeek changed Web API. Run npm run doctor, then npm run auth.'); } - const answer = await solvePOW(challenge, account.config); + const answer = await solvePOW(challenge, account.config.wasmUrl); if (!session.id) { - const sr = await fetch('https://chat.deepseek.com/api/v0/chat_session/create', { + const sr = await dsFetch('https://chat.deepseek.com/api/v0/chat_session/create', { method: 'POST', headers: dsHeaders, body: '{}' }); const { json: sessionData, text: sessionText } = await readDeepSeekJsonResponse(sr, 'session create', account); @@ -463,7 +451,7 @@ async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default') { salt: challenge.salt, answer: answer, signature: challenge.signature, target_path: '/api/v0/chat/completion' })).toString('base64'); - const resp = await fetch('https://chat.deepseek.com/api/v0/chat/completion', { + const resp = await dsFetch('https://chat.deepseek.com/api/v0/chat/completion', { method: 'POST', headers: { ...dsHeaders, 'X-DS-PoW-Response': powB64 }, body: JSON.stringify({ @@ -489,7 +477,7 @@ async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default') { session.createdAt = null; session.messageCount = 0; - const sr2 = await fetch('https://chat.deepseek.com/api/v0/chat_session/create', { + const sr2 = await dsFetch('https://chat.deepseek.com/api/v0/chat_session/create', { method: 'POST', headers: dsHeaders, body: '{}' }); const { json: sessionData2, text: sessionText2 } = await readDeepSeekJsonResponse(sr2, 'session recreate', account); @@ -508,7 +496,7 @@ async function askDeepSeekStream(prompt, agentId, model = 'deepseek-default') { salt: challenge.salt, answer: answer, signature: challenge.signature, target_path: '/api/v0/chat/completion' })).toString('base64'); - const resp2 = await fetch('https://chat.deepseek.com/api/v0/chat/completion', { + const resp2 = await dsFetch('https://chat.deepseek.com/api/v0/chat/completion', { method: 'POST', headers: { ...dsHeaders, 'X-DS-PoW-Response': newPowB64 }, body: JSON.stringify({ @@ -1311,8 +1299,9 @@ const server = http.createServer(async (req, res) => { rebuildFragmentState(); }; + const decoder = new TextDecoder(); // one instance: preserves multi-byte (Cyrillic/emoji) split across chunks for await (const chunk of readable) { - buffer += new TextDecoder().decode(chunk, { stream: true }); + buffer += decoder.decode(chunk, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) {