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 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 immediately: the input is already verified by _setInputText, and a
2128
- // clickable send button normally appears right away. The old code slept a
2129
- // fixed 50ms here, which only added latency (the button does not need it).
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
- for (const sel of SEND_SELECTORS) {
2132
- const btn = this.page.locator(sel).last();
2133
- try {
2134
- if ((await btn.count()) === 0)
2135
- continue;
2136
- if (!(await btn.isVisible().catch(() => false)))
2137
- continue;
2138
- await btn.click({ timeout: 1500 });
2139
- sent = true;
2140
- break;
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
- catch { }
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.44.0",
3
+ "version": "2.46.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {