zames_pro 2.0.0 → 2.1.1

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.
@@ -1,11 +1,29 @@
1
1
  import { buildSystemPrompt } from './system-prompt.js';
2
2
  import { getGitContext, formatGitContext } from './gitTools.js';
3
3
  import { parseXmlToolCalls } from './xml-toolcall.js';
4
- export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 40, freshChat = false, sendSystemPrompt = false, transcript = null, onThinking = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, debugLog = false, }) {
4
+ export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 40, freshChat = false, sendSystemPrompt = false, transcript = null, onThinking = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, debugLog = false, }) {
5
5
  if (freshChat) {
6
6
  await browser.newChat();
7
7
  transcript?.log('new_chat');
8
8
  }
9
+ // Сообщаем вызывающему актуальный chat id. После newChat() URL ещё без id
10
+ // (он появляется только после первой отправки), поэтому зовём колбэк и
11
+ // здесь, и после первой реальной отправки ниже.
12
+ let lastReportedChatId = null;
13
+ const reportChat = async () => {
14
+ let id = null;
15
+ try {
16
+ id = await browser.getCurrentChatId();
17
+ }
18
+ catch {
19
+ id = null;
20
+ }
21
+ if (id && id !== lastReportedChatId) {
22
+ lastReportedChatId = id;
23
+ onChatReady(id);
24
+ }
25
+ };
26
+ await reportChat();
9
27
  if (sendSystemPrompt) {
10
28
  let gitText = null;
11
29
  try {
@@ -26,6 +44,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
26
44
  });
27
45
  onThinking();
28
46
  await browser.ask(systemPrompt, { timeout: 60_000 });
47
+ await reportChat();
29
48
  }
30
49
  let message = task;
31
50
  transcript?.log('task', { task });
@@ -37,6 +56,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
37
56
  for (let i = 0; i < maxIterations; i++) {
38
57
  onThinking();
39
58
  const rawResponse = await browser.ask(message);
59
+ await reportChat();
40
60
  transcript?.log('assistant_raw', { response: rawResponse });
41
61
  const parsed = parseToolCall(rawResponse);
42
62
  if (parsed) {
@@ -60,9 +80,16 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
60
80
  const looksLikeToolCall = /("tool"\s*:|\btool_calls?\b|\binvoke\b|\bparameter\b|DSML|function_call)/i.test(rawResponse) || /\{\s*"?(tool|name)"?\s*:/.test(rawResponse);
61
81
  if (looksLikeToolCall && malformedRetries < MAX_MALFORMED_RETRIES) {
62
82
  malformedRetries++;
63
- transcript?.log("malformed_toolcall", { attempt: malformedRetries, response: rawResponse });
83
+ transcript?.log('malformed_toolcall', {
84
+ attempt: malformedRetries,
85
+ response: rawResponse,
86
+ });
64
87
  if (debugLog) {
65
- console.error("внимание: ответ похож на tool-call, но не распознан (попытка " + malformedRetries + "/" + MAX_MALFORMED_RETRIES + ")");
88
+ console.error('внимание: ответ похож на tool-call, но не распознан (попытка ' +
89
+ malformedRetries +
90
+ '/' +
91
+ MAX_MALFORMED_RETRIES +
92
+ ')');
66
93
  }
67
94
  message =
68
95
  'Твой предыдущий ответ не распознан как вызов инструмента. ' +
@@ -649,4 +676,3 @@ function extractPreToolText(text) {
649
676
  return '';
650
677
  return text.slice(0, earliest).trim();
651
678
  }
652
- //# sourceMappingURL=agent-loop.js.map
package/dist/browser.js CHANGED
@@ -1,4 +1,5 @@
1
- import { chromium } from 'playwright';
1
+ import { chromium, } from 'playwright';
2
+ import { extractAnswer, dumpNetBody } from './net-capture.js';
2
3
  import path from 'path';
3
4
  import os from 'os';
4
5
  import fs from 'fs/promises';
@@ -12,10 +13,25 @@ const INPUT_SELECTORS = [
12
13
  '[role="textbox"]',
13
14
  ];
14
15
  const ANSWER_SELECTORS = [
16
+ 'div.ds-assistant-message-main-content',
17
+ 'div[class*="ds-assistant-message-main-content"]',
15
18
  'div[class*="ds-markdown"]',
16
19
  'div[class*="markdown"]',
17
20
  ];
21
+ const SEND_SELECTORS = [
22
+ 'div[role="button"].ds-button--primary.ds-button--circle',
23
+ 'div[role="button"].ds-button--primary.ds-button--filled',
24
+ 'button[type="submit"]',
25
+ 'button[aria-label*="send" i]',
26
+ 'button[aria-label*="отправ" i]',
27
+ ];
28
+ // ВАЖНО: сюда НЕЛЬЗЯ добавлять общий 'div[role="button"][class*="ds-button--primary"]'
29
+ // — под него попадает кнопка отправки, которая видна всегда, и тогда
30
+ // _isGenerating() вечно возвращает true, из-за чего ответ никогда не
31
+ // считается готовым.
18
32
  const STOP_SELECTORS = [
33
+ 'div[role="button"][aria-label*="stop" i]',
34
+ 'div[role="button"][aria-label*="останов" i]',
19
35
  'button:has-text("Stop")',
20
36
  'button:has-text("Остановить")',
21
37
  'button[aria-label*="Stop" i]',
@@ -71,6 +87,12 @@ export class DeepSeekBrowser {
71
87
  _abort;
72
88
  context;
73
89
  page;
90
+ _netCapture;
91
+ _netCaptureAt;
92
+ _netChatId;
93
+ _netSniff;
94
+ _netSniffLimit;
95
+ _netHookInstalled;
74
96
  constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 3, stabilityDelayMs = 1000, minSendIntervalMs = 15000, } = {}) {
75
97
  this.headless = headless;
76
98
  this.debug = debug;
@@ -79,11 +101,15 @@ export class DeepSeekBrowser {
79
101
  this.askRetries = askRetries;
80
102
  this.stabilityChecks = stabilityChecks;
81
103
  this.stabilityDelayMs = stabilityDelayMs;
82
- // Минимальный интервал между отправками в чат: DeepSeek ограничивает
83
- // частоту («Messages too frequent. Try again later.»).
84
104
  this.minSendIntervalMs = minSendIntervalMs;
85
105
  this._lastSentAt = 0;
86
106
  this._abort = false;
107
+ this._netCapture = '';
108
+ this._netCaptureAt = 0;
109
+ this._netChatId = null;
110
+ this._netSniff = [];
111
+ this._netSniffLimit = 5;
112
+ this._netHookInstalled = false;
87
113
  }
88
114
  async launch() {
89
115
  await killStaleChrome();
@@ -126,8 +152,51 @@ export class DeepSeekBrowser {
126
152
  }
127
153
  }
128
154
  this.page = this.context.pages()[0] || (await this.context.newPage());
155
+ this._installNetHook();
129
156
  await this.page.goto(CHAT_URL, { waitUntil: 'domcontentloaded' });
130
157
  }
158
+ _installNetHook() {
159
+ if (this._netHookInstalled)
160
+ return;
161
+ const pg = this['page'];
162
+ if (!pg)
163
+ return;
164
+ this._netHookInstalled = true;
165
+ pg.on('response', (resp) => {
166
+ void this._onResponse(resp).catch(() => { });
167
+ });
168
+ }
169
+ async _onResponse(resp) {
170
+ try {
171
+ const url = resp.url();
172
+ if (!/deepseek\.com/i.test(url))
173
+ return;
174
+ if (/\.(js|css|png|jpg|jpeg|svg|woff2?|ico|map)(\?|$)/i.test(url))
175
+ return;
176
+ const ct = (resp.headers()['content-type'] || '').toLowerCase();
177
+ const interesting = ct.includes('event-stream') ||
178
+ ct.includes('json') ||
179
+ ct.includes('text/plain');
180
+ if (!interesting)
181
+ return;
182
+ const body = await resp.text().catch(() => '');
183
+ if (!body)
184
+ return;
185
+ this._netSniff.push({ url, contentType: ct, body });
186
+ if (this._netSniff.length > this._netSniffLimit)
187
+ this._netSniff.shift();
188
+ void dumpNetBody(url, body);
189
+ const extracted = extractAnswer(body);
190
+ if (extracted) {
191
+ this._netCapture = extracted;
192
+ const cid = url.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
193
+ if (cid)
194
+ this._netChatId = cid[0];
195
+ this._netCaptureAt = Date.now();
196
+ }
197
+ }
198
+ catch { }
199
+ }
131
200
  async restart() {
132
201
  try {
133
202
  if (this.context)
@@ -193,6 +262,12 @@ export class DeepSeekBrowser {
193
262
  return null;
194
263
  }
195
264
  async _readLastAnswerText() {
265
+ // Если удалось перехватить сырой текст ответа по сети (без рендер-
266
+ // искажений DeepSeek) и он относится к текущему ответу — отдаём его.
267
+ // Это защищает $, экранированные переводы строк и т.п. в аргументах.
268
+ if (this._netCapture && this._netCaptureAt >= this._lastSentAt) {
269
+ return this._netCapture;
270
+ }
196
271
  return await this.page.evaluate((sels) => {
197
272
  let el = null;
198
273
  for (const s of sels) {
@@ -202,74 +277,8 @@ export class DeepSeekBrowser {
202
277
  }
203
278
  if (!el)
204
279
  return '';
205
- const clone = el.cloneNode(true);
206
- Array.from(clone.querySelectorAll("pre")).forEach((pre) => {
207
- const codeEl = pre.querySelector('code');
208
- const source = codeEl || pre;
209
- const text = (source.textContent || '').replace(/\n$/, '');
210
- const langMatch = (source.className || '').match(/language-([\w-]+)/);
211
- const lang = langMatch ? langMatch[1] : '';
212
- const replacement = document.createTextNode('```' + lang + '\n' + text + '\n```');
213
- if (pre.parentNode)
214
- pre.parentNode.replaceChild(replacement, pre);
215
- });
216
- Array.from(clone.querySelectorAll("code")).forEach((c) => {
217
- const replacement = document.createTextNode('`' + (c.textContent || '') + '`');
218
- if (c.parentNode)
219
- c.parentNode.replaceChild(replacement, c);
220
- });
221
- // textContent склеивает блоки без переводов строк, из-за чего
222
- // Markdown-рендер получает одну длинную строку. Обходим DOM сами и
223
- // расставляем переводы строк / маркеры Markdown по блочным элементам.
280
+ const out = el.innerText || el.textContent || '';
224
281
  const NL = String.fromCharCode(10);
225
- const BULLET = String.fromCharCode(45) + ' '; // '- '
226
- function domToMarkdown(node) {
227
- if (node.nodeType === 3)
228
- return node.textContent || '';
229
- if (node.nodeType !== 1)
230
- return '';
231
- const tag = node.tagName.toLowerCase();
232
- if (tag === 'br')
233
- return NL;
234
- const inner = Array.from(node.childNodes)
235
- .map(domToMarkdown)
236
- .join('');
237
- const STAR = String.fromCharCode(42); // '*'
238
- if (tag === 'strong' || tag === 'b')
239
- return STAR + STAR + inner + STAR + STAR;
240
- if (tag === 'em' || tag === 'i')
241
- return STAR + inner + STAR;
242
- if (tag === 'del' || tag === 's')
243
- return '~~' + inner + '~~';
244
- if (tag === 'a') {
245
- const href = node.getAttribute('href') || '';
246
- return href ? '[' + inner + '](' + href + ')' : inner;
247
- }
248
- if (/^h[1-6]$/.test(tag)) {
249
- const level = Number(tag[1]);
250
- const hashes = '#'.repeat(level);
251
- return NL + NL + hashes + ' ' + inner.trim() + NL + NL;
252
- }
253
- if (tag === 'li') {
254
- const text = inner.trim().replace(new RegExp(NL + '+', 'g'), ' ');
255
- return BULLET + text + NL;
256
- }
257
- if (tag === 'ul' || tag === 'ol' || tag === 'blockquote') {
258
- return NL + inner + NL;
259
- }
260
- if (tag === 'p' ||
261
- tag === 'div' ||
262
- tag === 'section' ||
263
- tag === 'article' ||
264
- tag === 'tr' ||
265
- tag === 'table') {
266
- const text = inner.trim();
267
- return text ? NL + NL + text : '';
268
- }
269
- return inner;
270
- }
271
- const out = domToMarkdown(clone);
272
- // Схлопываем тройные+ переводы строк до двойных (разделитель блоков).
273
282
  return out.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim();
274
283
  }, ANSWER_SELECTORS);
275
284
  }
@@ -278,7 +287,6 @@ export class DeepSeekBrowser {
278
287
  const t = (raw || '').trim();
279
288
  if (!t)
280
289
  return '';
281
- // Отсекаем служебные статусы интерфейса (Reading..., Думаю...).
282
290
  if (STATUS_RE.test(t))
283
291
  return '';
284
292
  return raw;
@@ -287,8 +295,6 @@ export class DeepSeekBrowser {
287
295
  const stop = await this._findVisible(STOP_SELECTORS, 300);
288
296
  return !!stop;
289
297
  }
290
- // Прервать текущую генерацию: нажать Stop в интерфейсе.
291
- // Используется при нажатии Esc пользователем.
292
298
  async stopGeneration() {
293
299
  this._abort = true;
294
300
  const btn = await this._findVisible(STOP_SELECTORS, 500);
@@ -327,14 +333,6 @@ export class DeepSeekBrowser {
327
333
  }
328
334
  throw new Error(`ask() провалился после ${this.askRetries} попыток: ${lastErr?.message}`);
329
335
  }
330
- // Вставка текста в поле ввода.
331
- //
332
- // fill()/insertText() ломаются на многострочном тексте в contenteditable-
333
- // редакторах (ProseMirror/Lexical/...): символ перевода строки там
334
- // трактуется как Enter, и в поле остаётся только первая строка. Поэтому
335
- // для contenteditable и [role=textbox] эмулируем paste-событие с полным
336
- // текстом — то же, что делает Shift+Insert. Для нативных textarea/input
337
- // перевод строки работает и так.
338
336
  async _setInputText(input, text) {
339
337
  const tag = await input.evaluate((el) => el.tagName.toLowerCase());
340
338
  const isNative = tag === 'textarea' || tag === 'input';
@@ -346,7 +344,6 @@ export class DeepSeekBrowser {
346
344
  catch { }
347
345
  }
348
346
  await input.click();
349
- // Выделяем всё содержимое, чтобы вставка заменила его целиком.
350
347
  await this.page.keyboard.press('Control+A');
351
348
  await this.page.keyboard.press('Delete');
352
349
  const ok = await input.evaluate((el, value) => {
@@ -359,32 +356,30 @@ export class DeepSeekBrowser {
359
356
  cancelable: true,
360
357
  });
361
358
  el.dispatchEvent(ev);
362
- // Если обработчик не отменил вставку и поле осталось пустым —
363
- // пробуем через insertText вручную (fallback ниже).
364
359
  return true;
365
360
  }, text);
366
361
  if (!ok) {
367
362
  await this.page.keyboard.insertText(text);
368
363
  }
369
- // Проверяем, что текст реально попал в поле. Если редактор проигнорировал
370
- // paste — падаем на insertText.
371
364
  const got = await input.evaluate((el) => {
372
- if (el.tagName.toLowerCase() === 'textarea' || el.tagName.toLowerCase() === 'input') {
365
+ if (el.tagName.toLowerCase() === 'textarea' ||
366
+ el.tagName.toLowerCase() === 'input') {
373
367
  return el.value;
374
368
  }
375
369
  return el.innerText || el.textContent || '';
376
370
  });
377
- const norm = (s) => (s || '').replace(/\r\n/g, '\n').replace(/\u00a0/g, ' ').trim();
371
+ const norm = (s) => (s || '')
372
+ .replace(/\r\n/g, '\n')
373
+ .replace(/\u00a0/g, ' ')
374
+ .trim();
378
375
  if (!norm(got)) {
379
376
  await input.click();
380
377
  await this.page.keyboard.insertText(text);
381
378
  }
382
379
  }
383
- // Ждём, пока с прошлой отправки пройдёт minSendIntervalMs. Защита от
384
- // «Messages too frequent. Try again later.» на [chat.deepseek.com](https://chat.deepseek.com/).
385
380
  async _waitForSendSlot() {
386
381
  if (!this._lastSentAt)
387
- return; // первая отправка — пауза не нужна
382
+ return;
388
383
  const gap = this.minSendIntervalMs - (Date.now() - this._lastSentAt);
389
384
  if (gap <= 0)
390
385
  return;
@@ -398,25 +393,42 @@ export class DeepSeekBrowser {
398
393
  }
399
394
  const beforeText = await this._readLastAnswerTextClean().catch(() => '');
400
395
  await this._waitForSendSlot();
396
+ this._netCapture = '';
397
+ this._netCaptureAt = 0;
401
398
  await this._setInputText(input, prompt);
402
399
  await this.page.waitForTimeout(200);
403
- const sendBtn = this.page
404
- .locator('button')
405
- .filter({ hasText: /send|отправить/i })
406
- .first();
407
- try {
408
- await sendBtn.click({ timeout: 1500 });
400
+ let sent = false;
401
+ for (const sel of SEND_SELECTORS) {
402
+ const btn = this.page.locator(sel).last();
403
+ try {
404
+ if ((await btn.count()) === 0)
405
+ continue;
406
+ if (!(await btn.isVisible().catch(() => false)))
407
+ continue;
408
+ await btn.click({ timeout: 1500 });
409
+ sent = true;
410
+ break;
411
+ }
412
+ catch { }
409
413
  }
410
- catch {
414
+ if (!sent) {
411
415
  await this.page.keyboard.press('Enter');
412
416
  }
413
417
  this._lastSentAt = Date.now();
418
+ // Ждём старта: либо появился Stop, либо изменился текст ответа,
419
+ // либо вырос общий объём текста на странице.
414
420
  const startDeadline = Date.now() + 15_000;
421
+ const startBodyLen = await this.page
422
+ .evaluate(() => document.body.innerText.length)
423
+ .catch(() => 0);
415
424
  let started = false;
416
425
  while (Date.now() < startDeadline) {
417
426
  const gen = await this._isGenerating();
418
427
  const cur = await this._readLastAnswerTextClean().catch(() => '');
419
- if (gen || (cur && cur !== beforeText)) {
428
+ const bodyLen = await this.page
429
+ .evaluate(() => document.body.innerText.length)
430
+ .catch(() => 0);
431
+ if (gen || (cur && cur !== beforeText) || bodyLen > startBodyLen) {
420
432
  started = true;
421
433
  break;
422
434
  }
@@ -425,18 +437,20 @@ export class DeepSeekBrowser {
425
437
  if (!started) {
426
438
  throw new Error('Ответ не начал генерироваться за 15с. Возможно, сообщение не отправилось.');
427
439
  }
440
+ // Ждём, пока ответ перестанет меняться. Условие "не генерируется"
441
+ // проверяем через рост текста, а НЕ через _isGenerating(): кнопка Stop
442
+ // у DeepSeek ненадёжна, и если она не находится, ответ бы никогда не
443
+ // вернулся.
428
444
  const deadline = Date.now() + timeout;
429
445
  let last = '';
430
446
  let stable = 0;
431
447
  this._abort = false;
432
448
  while (Date.now() < deadline) {
433
449
  if (this._abort) {
434
- // Пользователь нажал Esc — вернём то, что успело сгенерироваться.
435
450
  return last || '(прервано пользователем)';
436
451
  }
437
- const gen = await this._isGenerating();
438
452
  const cur = await this._readLastAnswerTextClean().catch(() => '');
439
- if (cur && cur === last && !gen) {
453
+ if (cur && cur === last) {
440
454
  stable++;
441
455
  if (stable >= 2)
442
456
  return cur;
@@ -475,7 +489,6 @@ export class DeepSeekBrowser {
475
489
  });
476
490
  return { file: filePath, selectors: report };
477
491
  }
478
- // ---------- список чатов ----------
479
492
  async _ensureSidebarOpen() {
480
493
  const toggles = [
481
494
  'button[aria-label*="sidebar" i]',
@@ -555,10 +568,12 @@ export class DeepSeekBrowser {
555
568
  try {
556
569
  const url = this.page.url();
557
570
  const m = url.match(/\/chat\/s\/([a-zA-Z0-9_-]+)/);
558
- return m ? m[1] : null;
571
+ if (m)
572
+ return m[1];
573
+ return this._netChatId;
559
574
  }
560
575
  catch {
561
- return null;
576
+ return this._netChatId;
562
577
  }
563
578
  }
564
579
  async close() {
@@ -570,4 +585,3 @@ export class DeepSeekBrowser {
570
585
  await killStaleChrome();
571
586
  }
572
587
  }
573
- //# sourceMappingURL=browser.js.map
package/dist/config.js CHANGED
@@ -71,4 +71,3 @@ function deepMerge(target, source) {
71
71
  }
72
72
  export const CONFIG_PATHS = { HOME_CONFIG, PROJECT_CONFIG };
73
73
  export { ZAMES_HOME };
74
- //# sourceMappingURL=config.js.map
package/dist/confirm.js CHANGED
@@ -79,4 +79,3 @@ export function formatDiffPreview(oldText, newText, { label } = {}) {
79
79
  const diff = unifiedDiff(oldText, newText, { label });
80
80
  return colorDiff(diff);
81
81
  }
82
- //# sourceMappingURL=confirm.js.map
package/dist/diff.js CHANGED
@@ -35,4 +35,3 @@ export function colorDiff(diffText) {
35
35
  .join(String.fromCharCode(10));
36
36
  }
37
37
  export default { unifiedDiff, colorDiff };
38
- //# sourceMappingURL=diff.js.map
package/dist/gitTools.js CHANGED
@@ -216,4 +216,3 @@ export function createGitTools(workdir) {
216
216
  },
217
217
  ];
218
218
  }
219
- //# sourceMappingURL=gitTools.js.map