zames_pro 2.44.0 → 2.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.js +127 -15
- package/dist/commands.js +5 -0
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -87,6 +87,65 @@ export function askPassword(question) {
|
|
|
87
87
|
}
|
|
88
88
|
const USER_DATA_DIR = path.join(os.homedir(), '.zames', 'profile');
|
|
89
89
|
const CHAT_URL = 'https://chat.deepseek.com/';
|
|
90
|
+
// Cache of the sanitized (non-Headless) User-Agent, keyed by the browser
|
|
91
|
+
// engine path. On the FIRST headless run we detect the real UA, strip the
|
|
92
|
+
// "Headless" marker and store it here; later runs pass it straight to
|
|
93
|
+
// _launchOnce, so the browser starts with a good UA and the "launch → read UA →
|
|
94
|
+
// relaunch" dance happens only once (it otherwise doubled every headless
|
|
95
|
+
// start). Keying by the executable path means a Playwright update (new engine
|
|
96
|
+
// path) invalidates the cache and we re-derive the UA once for the new build.
|
|
97
|
+
const HEADLESS_UA_CACHE = path.join(os.homedir(), '.zames', 'headless-ua.json');
|
|
98
|
+
// Pure: pick a reusable UA from the cache file's text for the given engine, or
|
|
99
|
+
// null. Exported so the policy is unit-tested without touching ~/.zames.
|
|
100
|
+
export function parseHeadlessUACache(raw, engine) {
|
|
101
|
+
try {
|
|
102
|
+
const j = JSON.parse(raw);
|
|
103
|
+
if (j &&
|
|
104
|
+
j.engine === engine &&
|
|
105
|
+
typeof j.ua === 'string' &&
|
|
106
|
+
j.ua &&
|
|
107
|
+
!/Headless/i.test(j.ua)) {
|
|
108
|
+
return j.ua;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
function headlessEngineKey() {
|
|
115
|
+
try {
|
|
116
|
+
return chromium.executablePath();
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function readCachedHeadlessUA(engine) {
|
|
123
|
+
if (!engine)
|
|
124
|
+
return null;
|
|
125
|
+
try {
|
|
126
|
+
const raw = await fs.readFile(HEADLESS_UA_CACHE, 'utf-8');
|
|
127
|
+
return parseHeadlessUACache(raw, engine);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function writeCachedHeadlessUA(engine, ua) {
|
|
134
|
+
if (!engine || !ua)
|
|
135
|
+
return;
|
|
136
|
+
try {
|
|
137
|
+
await fs.mkdir(path.dirname(HEADLESS_UA_CACHE), { recursive: true });
|
|
138
|
+
await fs.writeFile(HEADLESS_UA_CACHE, JSON.stringify({ engine, ua }, null, 2), 'utf-8');
|
|
139
|
+
}
|
|
140
|
+
catch { }
|
|
141
|
+
}
|
|
142
|
+
// For /doctor: is a sanitized UA cached for the CURRENT engine build? A missing
|
|
143
|
+
// cache only means the next headless start relaunches once (not an error), so
|
|
144
|
+
// this is informational. Best-effort.
|
|
145
|
+
export async function headlessUACacheReady() {
|
|
146
|
+
const ua = await readCachedHeadlessUA(headlessEngineKey());
|
|
147
|
+
return !!ua;
|
|
148
|
+
}
|
|
90
149
|
// A headless Chrome advertises "HeadlessChrome/..." in its User-Agent, and
|
|
91
150
|
// DeepSeek's CDN (CloudFront/WAF) rejects that UA with a plain "403 ERROR"
|
|
92
151
|
// page before the app is even served — so a headless login looked broken while
|
|
@@ -542,6 +601,14 @@ export class DeepSeekBrowser {
|
|
|
542
601
|
console.error('profile: удаляю Singleton-файлы');
|
|
543
602
|
await cleanSingletonFiles();
|
|
544
603
|
}
|
|
604
|
+
// Headless: if we already know the sanitized UA for THIS engine build, use
|
|
605
|
+
// it from the start — that skips the "launch, read UA, relaunch" dance
|
|
606
|
+
// (which otherwise doubled every headless start).
|
|
607
|
+
if (this.headless && !this.userAgent) {
|
|
608
|
+
const cached = await readCachedHeadlessUA(headlessEngineKey());
|
|
609
|
+
if (cached)
|
|
610
|
+
this.userAgent = cached;
|
|
611
|
+
}
|
|
545
612
|
await this._launchOnce();
|
|
546
613
|
await this._fixHeadlessUserAgent();
|
|
547
614
|
}
|
|
@@ -563,6 +630,9 @@ export class DeepSeekBrowser {
|
|
|
563
630
|
if (fixed === ua)
|
|
564
631
|
return;
|
|
565
632
|
this.userAgent = fixed;
|
|
633
|
+
// Remember the good UA for this engine build so the NEXT headless start
|
|
634
|
+
// uses it immediately (no relaunch). Keyed by executable path.
|
|
635
|
+
void writeCachedHeadlessUA(headlessEngineKey(), fixed);
|
|
566
636
|
if (this.debug)
|
|
567
637
|
console.error('profile: headless UA → перезапуск с обычным Chrome UA');
|
|
568
638
|
await this.context.close().catch(() => { });
|
|
@@ -2124,28 +2194,70 @@ export class DeepSeekBrowser {
|
|
|
2124
2194
|
finalPrompt = rewriteFailedAttachments(finalPrompt, failed);
|
|
2125
2195
|
}
|
|
2126
2196
|
await this._setInputText(input, finalPrompt);
|
|
2127
|
-
// Send
|
|
2128
|
-
//
|
|
2129
|
-
//
|
|
2197
|
+
// Send: wait for the send button to become ENABLED first. After an
|
|
2198
|
+
// attachment upload the button is DISABLED while the file is still
|
|
2199
|
+
// uploading; a click on a disabled button silently does nothing, so the
|
|
2200
|
+
// message stayed in the input and no generation started (the operator saw
|
|
2201
|
+
// the text + the file in the box, but nothing was sent). We poll for an
|
|
2202
|
+
// ENABLED button, then click; if none becomes enabled in time, fall back
|
|
2203
|
+
// to Enter (which is also ignored while the send is disabled).
|
|
2204
|
+
const sendDeadline = Date.now() + 8000;
|
|
2130
2205
|
let sent = false;
|
|
2131
|
-
|
|
2132
|
-
const
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2206
|
+
while (!sent && Date.now() < sendDeadline) {
|
|
2207
|
+
for (const sel of SEND_SELECTORS) {
|
|
2208
|
+
const btn = this.page.locator(sel).last();
|
|
2209
|
+
try {
|
|
2210
|
+
if ((await btn.count()) === 0)
|
|
2211
|
+
continue;
|
|
2212
|
+
if (!(await btn.isVisible().catch(() => false)))
|
|
2213
|
+
continue;
|
|
2214
|
+
// DeepSeek's send control is a div[role=button]; Playwright's
|
|
2215
|
+
// isEnabled() does NOT honor aria-disabled on a div, so read the
|
|
2216
|
+
// attribute (and a disabled class) directly — during an upload the
|
|
2217
|
+
// button is aria-disabled=true and a click is silently ignored.
|
|
2218
|
+
const enabled = await btn
|
|
2219
|
+
.evaluate((el) => {
|
|
2220
|
+
if (el.getAttribute('aria-disabled') === 'true')
|
|
2221
|
+
return false;
|
|
2222
|
+
const cls = (el.className || '').toString();
|
|
2223
|
+
if (/disabled|is-disabled/.test(cls))
|
|
2224
|
+
return false;
|
|
2225
|
+
return true;
|
|
2226
|
+
})
|
|
2227
|
+
.catch(() => true);
|
|
2228
|
+
if (!enabled)
|
|
2229
|
+
continue;
|
|
2230
|
+
await btn.click({ timeout: 1500 });
|
|
2231
|
+
sent = true;
|
|
2232
|
+
break;
|
|
2233
|
+
}
|
|
2234
|
+
catch { }
|
|
2141
2235
|
}
|
|
2142
|
-
|
|
2236
|
+
if (!sent)
|
|
2237
|
+
await this.page.waitForTimeout(150);
|
|
2143
2238
|
}
|
|
2144
2239
|
if (!sent) {
|
|
2145
2240
|
await this.page.keyboard.press('Enter');
|
|
2146
2241
|
}
|
|
2147
2242
|
this._lastSentAt = Date.now();
|
|
2148
|
-
this._askDebug('SENT at=' + this._lastSentAt);
|
|
2243
|
+
this._askDebug('SENT at=' + this._lastSentAt + ' viaButton=' + sent);
|
|
2244
|
+
// Verify the send REALLY went out: DeepSeek clears the input when the
|
|
2245
|
+
// message is accepted. If the text is still there a moment later, the
|
|
2246
|
+
// click/Enter was ignored (a disabled button during upload, a lost
|
|
2247
|
+
// keystroke) — press Enter once more so the message is not silently stuck
|
|
2248
|
+
// in the box (exactly the operator's "text + file in the box, no send").
|
|
2249
|
+
try {
|
|
2250
|
+
await this.page.waitForTimeout(300);
|
|
2251
|
+
const stillThere = await input
|
|
2252
|
+
.evaluate((el) => (el.value || el.innerText || el.textContent || '').trim())
|
|
2253
|
+
.catch(() => '');
|
|
2254
|
+
if (stillThere) {
|
|
2255
|
+
this._askDebug('SEND not cleared, pressing Enter again');
|
|
2256
|
+
await this.page.keyboard.press('Enter');
|
|
2257
|
+
this._lastSentAt = Date.now();
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
catch { }
|
|
2149
2261
|
// Wait for the start: either Stop appeared, or the answer text changed,
|
|
2150
2262
|
// or the page grew (a new answer node / longer last answer). In parallel we
|
|
2151
2263
|
// catch the rate-limit toast (only toasts, not the whole body).
|
package/dist/commands.js
CHANGED
|
@@ -199,6 +199,11 @@ export function renderDoctor(d) {
|
|
|
199
199
|
if (typeof d.sshRemote === 'boolean') {
|
|
200
200
|
row(true, 'session', d.sshRemote ? 'SSH remote' : 'local terminal');
|
|
201
201
|
}
|
|
202
|
+
if (d.headless) {
|
|
203
|
+
row(!!d.headlessUaCached, 'headless', d.headlessUaCached
|
|
204
|
+
? 'UA cached (no relaunch)'
|
|
205
|
+
: 'no cached UA yet (first start relaunches once)');
|
|
206
|
+
}
|
|
202
207
|
return 'Doctor:' + NL + rows.join(NL);
|
|
203
208
|
}
|
|
204
209
|
export function renderPermissions(p) {
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'fs/promises';
|
|
|
4
4
|
import { existsSync, statSync } from 'fs';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { theme } from './theme.js';
|
|
7
|
-
import { DeepSeekBrowser } from './browser.js';
|
|
7
|
+
import { DeepSeekBrowser, headlessUACacheReady } from './browser.js';
|
|
8
8
|
import { createTools } from './tools.js';
|
|
9
9
|
import { runAgentLoop } from './agent-loop.js';
|
|
10
10
|
import { createSpinner } from './spinner.js';
|
|
@@ -2830,6 +2830,8 @@ async function main() {
|
|
|
2830
2830
|
: undefined,
|
|
2831
2831
|
minSendIntervalMs: config.browser.minSendIntervalMs,
|
|
2832
2832
|
sshRemote: !!(process.env.SSH_CONNECTION || process.env.SSH_TTY),
|
|
2833
|
+
headless: !!headless,
|
|
2834
|
+
headlessUaCached: await headlessUACacheReady().catch(() => false),
|
|
2833
2835
|
})));
|
|
2834
2836
|
continue;
|
|
2835
2837
|
}
|