zames_pro 2.1.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.
@@ -80,9 +80,16 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
80
80
  const looksLikeToolCall = /("tool"\s*:|\btool_calls?\b|\binvoke\b|\bparameter\b|DSML|function_call)/i.test(rawResponse) || /\{\s*"?(tool|name)"?\s*:/.test(rawResponse);
81
81
  if (looksLikeToolCall && malformedRetries < MAX_MALFORMED_RETRIES) {
82
82
  malformedRetries++;
83
- transcript?.log("malformed_toolcall", { attempt: malformedRetries, response: rawResponse });
83
+ transcript?.log('malformed_toolcall', {
84
+ attempt: malformedRetries,
85
+ response: rawResponse,
86
+ });
84
87
  if (debugLog) {
85
- console.error("внимание: ответ похож на tool-call, но не распознан (попытка " + malformedRetries + "/" + MAX_MALFORMED_RETRIES + ")");
88
+ console.error('внимание: ответ похож на tool-call, но не распознан (попытка ' +
89
+ malformedRetries +
90
+ '/' +
91
+ MAX_MALFORMED_RETRIES +
92
+ ')');
86
93
  }
87
94
  message =
88
95
  'Твой предыдущий ответ не распознан как вызов инструмента. ' +
package/dist/browser.js CHANGED
@@ -1,4 +1,4 @@
1
- import { chromium } from 'playwright';
1
+ import { chromium, } from 'playwright';
2
2
  import { extractAnswer, dumpNetBody } from './net-capture.js';
3
3
  import path from 'path';
4
4
  import os from 'os';
@@ -13,10 +13,25 @@ const INPUT_SELECTORS = [
13
13
  '[role="textbox"]',
14
14
  ];
15
15
  const ANSWER_SELECTORS = [
16
+ 'div.ds-assistant-message-main-content',
17
+ 'div[class*="ds-assistant-message-main-content"]',
16
18
  'div[class*="ds-markdown"]',
17
19
  'div[class*="markdown"]',
18
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
+ // считается готовым.
19
32
  const STOP_SELECTORS = [
33
+ 'div[role="button"][aria-label*="stop" i]',
34
+ 'div[role="button"][aria-label*="останов" i]',
20
35
  'button:has-text("Stop")',
21
36
  'button:has-text("Остановить")',
22
37
  'button[aria-label*="Stop" i]',
@@ -72,9 +87,6 @@ export class DeepSeekBrowser {
72
87
  _abort;
73
88
  context;
74
89
  page;
75
- // Перехват сетевых ответов DeepSeek: там лежит СЫРОЙ текст ответа модели
76
- // (markdown без рендер-искажений LaTeX/автолинков). Собираем его по мере
77
- // стрима, чтобы _readLastAnswerText отдавал исходник, а не DOM-рендер.
78
90
  _netCapture;
79
91
  _netCaptureAt;
80
92
  _netChatId;
@@ -89,8 +101,6 @@ export class DeepSeekBrowser {
89
101
  this.askRetries = askRetries;
90
102
  this.stabilityChecks = stabilityChecks;
91
103
  this.stabilityDelayMs = stabilityDelayMs;
92
- // Минимальный интервал между отправками в чат: DeepSeek ограничивает
93
- // частоту («Messages too frequent. Try again later.»).
94
104
  this.minSendIntervalMs = minSendIntervalMs;
95
105
  this._lastSentAt = 0;
96
106
  this._abort = false;
@@ -145,9 +155,6 @@ export class DeepSeekBrowser {
145
155
  this._installNetHook();
146
156
  await this.page.goto(CHAT_URL, { waitUntil: 'domcontentloaded' });
147
157
  }
148
- // Перехват сетевых ответов DeepSeek. Ответ модели приходит стримом
149
- // (SSE/JSON) — это СЫРОЙ markdown без рендер-искажений. Накапливаем его,
150
- // чтобы чтение ответа отдавало исходник, а не DOM-рендер.
151
158
  _installNetHook() {
152
159
  if (this._netHookInstalled)
153
160
  return;
@@ -270,74 +277,8 @@ export class DeepSeekBrowser {
270
277
  }
271
278
  if (!el)
272
279
  return '';
273
- const clone = el.cloneNode(true);
274
- Array.from(clone.querySelectorAll("pre")).forEach((pre) => {
275
- const codeEl = pre.querySelector('code');
276
- const source = codeEl || pre;
277
- const text = (source.textContent || '').replace(/\n$/, '');
278
- const langMatch = (source.className || '').match(/language-([\w-]+)/);
279
- const lang = langMatch ? langMatch[1] : '';
280
- const replacement = document.createTextNode('```' + lang + '\n' + text + '\n```');
281
- if (pre.parentNode)
282
- pre.parentNode.replaceChild(replacement, pre);
283
- });
284
- Array.from(clone.querySelectorAll("code")).forEach((c) => {
285
- const replacement = document.createTextNode('`' + (c.textContent || '') + '`');
286
- if (c.parentNode)
287
- c.parentNode.replaceChild(replacement, c);
288
- });
289
- // textContent склеивает блоки без переводов строк, из-за чего
290
- // Markdown-рендер получает одну длинную строку. Обходим DOM сами и
291
- // расставляем переводы строк / маркеры Markdown по блочным элементам.
280
+ const out = el.innerText || el.textContent || '';
292
281
  const NL = String.fromCharCode(10);
293
- const BULLET = String.fromCharCode(45) + ' '; // '- '
294
- function domToMarkdown(node) {
295
- if (node.nodeType === 3)
296
- return node.textContent || '';
297
- if (node.nodeType !== 1)
298
- return '';
299
- const tag = node.tagName.toLowerCase();
300
- if (tag === 'br')
301
- return NL;
302
- const inner = Array.from(node.childNodes)
303
- .map(domToMarkdown)
304
- .join('');
305
- const STAR = String.fromCharCode(42); // '*'
306
- if (tag === 'strong' || tag === 'b')
307
- return STAR + STAR + inner + STAR + STAR;
308
- if (tag === 'em' || tag === 'i')
309
- return STAR + inner + STAR;
310
- if (tag === 'del' || tag === 's')
311
- return '~~' + inner + '~~';
312
- if (tag === 'a') {
313
- const href = node.getAttribute('href') || '';
314
- return href ? '[' + inner + '](' + href + ')' : inner;
315
- }
316
- if (/^h[1-6]$/.test(tag)) {
317
- const level = Number(tag[1]);
318
- const hashes = '#'.repeat(level);
319
- return NL + NL + hashes + ' ' + inner.trim() + NL + NL;
320
- }
321
- if (tag === 'li') {
322
- const text = inner.trim().replace(new RegExp(NL + '+', 'g'), ' ');
323
- return BULLET + text + NL;
324
- }
325
- if (tag === 'ul' || tag === 'ol' || tag === 'blockquote') {
326
- return NL + inner + NL;
327
- }
328
- if (tag === 'p' ||
329
- tag === 'div' ||
330
- tag === 'section' ||
331
- tag === 'article' ||
332
- tag === 'tr' ||
333
- tag === 'table') {
334
- const text = inner.trim();
335
- return text ? NL + NL + text : '';
336
- }
337
- return inner;
338
- }
339
- const out = domToMarkdown(clone);
340
- // Схлопываем тройные+ переводы строк до двойных (разделитель блоков).
341
282
  return out.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim();
342
283
  }, ANSWER_SELECTORS);
343
284
  }
@@ -346,7 +287,6 @@ export class DeepSeekBrowser {
346
287
  const t = (raw || '').trim();
347
288
  if (!t)
348
289
  return '';
349
- // Отсекаем служебные статусы интерфейса (Reading..., Думаю...).
350
290
  if (STATUS_RE.test(t))
351
291
  return '';
352
292
  return raw;
@@ -355,8 +295,6 @@ export class DeepSeekBrowser {
355
295
  const stop = await this._findVisible(STOP_SELECTORS, 300);
356
296
  return !!stop;
357
297
  }
358
- // Прервать текущую генерацию: нажать Stop в интерфейсе.
359
- // Используется при нажатии Esc пользователем.
360
298
  async stopGeneration() {
361
299
  this._abort = true;
362
300
  const btn = await this._findVisible(STOP_SELECTORS, 500);
@@ -395,14 +333,6 @@ export class DeepSeekBrowser {
395
333
  }
396
334
  throw new Error(`ask() провалился после ${this.askRetries} попыток: ${lastErr?.message}`);
397
335
  }
398
- // Вставка текста в поле ввода.
399
- //
400
- // fill()/insertText() ломаются на многострочном тексте в contenteditable-
401
- // редакторах (ProseMirror/Lexical/...): символ перевода строки там
402
- // трактуется как Enter, и в поле остаётся только первая строка. Поэтому
403
- // для contenteditable и [role=textbox] эмулируем paste-событие с полным
404
- // текстом — то же, что делает Shift+Insert. Для нативных textarea/input
405
- // перевод строки работает и так.
406
336
  async _setInputText(input, text) {
407
337
  const tag = await input.evaluate((el) => el.tagName.toLowerCase());
408
338
  const isNative = tag === 'textarea' || tag === 'input';
@@ -414,7 +344,6 @@ export class DeepSeekBrowser {
414
344
  catch { }
415
345
  }
416
346
  await input.click();
417
- // Выделяем всё содержимое, чтобы вставка заменила его целиком.
418
347
  await this.page.keyboard.press('Control+A');
419
348
  await this.page.keyboard.press('Delete');
420
349
  const ok = await input.evaluate((el, value) => {
@@ -427,32 +356,30 @@ export class DeepSeekBrowser {
427
356
  cancelable: true,
428
357
  });
429
358
  el.dispatchEvent(ev);
430
- // Если обработчик не отменил вставку и поле осталось пустым —
431
- // пробуем через insertText вручную (fallback ниже).
432
359
  return true;
433
360
  }, text);
434
361
  if (!ok) {
435
362
  await this.page.keyboard.insertText(text);
436
363
  }
437
- // Проверяем, что текст реально попал в поле. Если редактор проигнорировал
438
- // paste — падаем на insertText.
439
364
  const got = await input.evaluate((el) => {
440
- if (el.tagName.toLowerCase() === 'textarea' || el.tagName.toLowerCase() === 'input') {
365
+ if (el.tagName.toLowerCase() === 'textarea' ||
366
+ el.tagName.toLowerCase() === 'input') {
441
367
  return el.value;
442
368
  }
443
369
  return el.innerText || el.textContent || '';
444
370
  });
445
- 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();
446
375
  if (!norm(got)) {
447
376
  await input.click();
448
377
  await this.page.keyboard.insertText(text);
449
378
  }
450
379
  }
451
- // Ждём, пока с прошлой отправки пройдёт minSendIntervalMs. Защита от
452
- // «Messages too frequent. Try again later.» на [chat.deepseek.com](https://chat.deepseek.com/).
453
380
  async _waitForSendSlot() {
454
381
  if (!this._lastSentAt)
455
- return; // первая отправка — пауза не нужна
382
+ return;
456
383
  const gap = this.minSendIntervalMs - (Date.now() - this._lastSentAt);
457
384
  if (gap <= 0)
458
385
  return;
@@ -466,28 +393,42 @@ export class DeepSeekBrowser {
466
393
  }
467
394
  const beforeText = await this._readLastAnswerTextClean().catch(() => '');
468
395
  await this._waitForSendSlot();
469
- // Сбрасываем прошлый перехват: ответ на это сообщение ещё придёт.
470
396
  this._netCapture = '';
471
397
  this._netCaptureAt = 0;
472
398
  await this._setInputText(input, prompt);
473
399
  await this.page.waitForTimeout(200);
474
- const sendBtn = this.page
475
- .locator('button')
476
- .filter({ hasText: /send|отправить/i })
477
- .first();
478
- try {
479
- 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 { }
480
413
  }
481
- catch {
414
+ if (!sent) {
482
415
  await this.page.keyboard.press('Enter');
483
416
  }
484
417
  this._lastSentAt = Date.now();
418
+ // Ждём старта: либо появился Stop, либо изменился текст ответа,
419
+ // либо вырос общий объём текста на странице.
485
420
  const startDeadline = Date.now() + 15_000;
421
+ const startBodyLen = await this.page
422
+ .evaluate(() => document.body.innerText.length)
423
+ .catch(() => 0);
486
424
  let started = false;
487
425
  while (Date.now() < startDeadline) {
488
426
  const gen = await this._isGenerating();
489
427
  const cur = await this._readLastAnswerTextClean().catch(() => '');
490
- 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) {
491
432
  started = true;
492
433
  break;
493
434
  }
@@ -496,18 +437,20 @@ export class DeepSeekBrowser {
496
437
  if (!started) {
497
438
  throw new Error('Ответ не начал генерироваться за 15с. Возможно, сообщение не отправилось.');
498
439
  }
440
+ // Ждём, пока ответ перестанет меняться. Условие "не генерируется"
441
+ // проверяем через рост текста, а НЕ через _isGenerating(): кнопка Stop
442
+ // у DeepSeek ненадёжна, и если она не находится, ответ бы никогда не
443
+ // вернулся.
499
444
  const deadline = Date.now() + timeout;
500
445
  let last = '';
501
446
  let stable = 0;
502
447
  this._abort = false;
503
448
  while (Date.now() < deadline) {
504
449
  if (this._abort) {
505
- // Пользователь нажал Esc — вернём то, что успело сгенерироваться.
506
450
  return last || '(прервано пользователем)';
507
451
  }
508
- const gen = await this._isGenerating();
509
452
  const cur = await this._readLastAnswerTextClean().catch(() => '');
510
- if (cur && cur === last && !gen) {
453
+ if (cur && cur === last) {
511
454
  stable++;
512
455
  if (stable >= 2)
513
456
  return cur;
@@ -518,19 +461,8 @@ export class DeepSeekBrowser {
518
461
  last = cur;
519
462
  await this.page.waitForTimeout(800);
520
463
  }
521
- if (last) {
522
- if (this.debug) {
523
- const src = this._netCapture && this._netCaptureAt >= this._lastSentAt
524
- ? 'NET'
525
- : 'DOM';
526
- console.error('[browser] ответ прочитан из: ' + src);
527
- if (this._netSniff.length) {
528
- console.error('[browser] перехваченные ответы: ' +
529
- this._netSniff.map((s) => s.url).join(', '));
530
- }
531
- }
464
+ if (last)
532
465
  return last;
533
- }
534
466
  throw new Error('Таймаут ожидания ответа. Попробуйте /debug-dom.');
535
467
  }
536
468
  async dumpDom(filePath) {
@@ -557,7 +489,6 @@ export class DeepSeekBrowser {
557
489
  });
558
490
  return { file: filePath, selectors: report };
559
491
  }
560
- // ---------- список чатов ----------
561
492
  async _ensureSidebarOpen() {
562
493
  const toggles = [
563
494
  'button[aria-label*="sidebar" i]',
package/dist/index.js CHANGED
@@ -76,19 +76,23 @@ function loadLastChat(workdir = '') {
76
76
  // их через динамический import с timestamp-query — Node кэширует ESM по URL,
77
77
  // поэтому такой import вернёт свежую версию модуля. Браузер, чат и текущее
78
78
  // состояние НЕ перезапускаются: обновляется только логика.
79
+ // В dev-режиме tsx грузит .ts-исходники из src/, а в собранном dist — .js.
80
+ // Динамический import с query ?t= идёт мимо переписывания расширений tsx,
81
+ // поэтому расширение подбираем сами — по фактическому файлу текущего модуля.
82
+ const SRC_EXT = /[.]ts$/.test(new URL(import.meta.url).pathname) ? '.ts' : '.js';
79
83
  const RELOADABLE = [
80
- './tools.js',
81
- './agent-loop.js',
82
- './system-prompt.js',
83
- './gitTools.js',
84
- './web.js',
85
- './self-review.js',
86
- './diff.js',
87
- './undo.js',
88
- './confirm.js',
89
- './transcript.js',
90
- './spinner.js',
91
- './config.js',
84
+ 'tools',
85
+ 'agent-loop',
86
+ 'system-prompt',
87
+ 'gitTools',
88
+ 'web',
89
+ 'self-review',
90
+ 'diff',
91
+ 'undo',
92
+ 'confirm',
93
+ 'transcript',
94
+ 'spinner',
95
+ 'config',
92
96
  ];
93
97
  const mod = {
94
98
  createTools,
@@ -105,36 +109,37 @@ async function reloadModules() {
105
109
  const stamp = Date.now();
106
110
  const loaded = new Map();
107
111
  const errors = [];
108
- for (const rel of RELOADABLE) {
112
+ for (const base of RELOADABLE) {
113
+ const rel = './' + base + SRC_EXT;
109
114
  try {
110
115
  const url = new URL(rel, import.meta.url);
111
116
  url.searchParams.set('t', String(stamp));
112
117
  const m = (await import(url.href));
113
- loaded.set(rel, m);
118
+ loaded.set(base, m);
114
119
  }
115
120
  catch (e) {
116
- errors.push(`${rel}: ${e.message}`);
121
+ errors.push(rel + ': ' + e.message);
117
122
  }
118
123
  }
119
- const pick = (rel, name) => loaded.get(rel)?.[name];
120
- if (pick('./tools.js', 'createTools'))
121
- mod.createTools = pick('./tools.js', 'createTools');
122
- if (pick('./agent-loop.js', 'runAgentLoop'))
123
- mod.runAgentLoop = pick('./agent-loop.js', 'runAgentLoop');
124
- if (pick('./system-prompt.js', 'buildSystemPrompt'))
125
- mod.buildSystemPrompt = pick('./system-prompt.js', 'buildSystemPrompt');
126
- if (pick('./self-review.js', 'selfReview'))
127
- mod.selfReview = pick('./self-review.js', 'selfReview');
128
- if (pick('./self-review.js', 'selfDiff'))
129
- mod.selfDiff = pick('./self-review.js', 'selfDiff');
130
- if (pick('./self-review.js', 'selfApply'))
131
- mod.selfApply = pick('./self-review.js', 'selfApply');
132
- if (pick('./self-review.js', 'selfList'))
133
- mod.selfList = pick('./self-review.js', 'selfList');
134
- if (pick('./web.js', 'closeWeb'))
135
- mod.closeWeb = pick('./web.js', 'closeWeb');
136
- if (pick('./spinner.js', 'createSpinner'))
137
- mod.createSpinner = pick('./spinner.js', 'createSpinner');
124
+ const pick = (base, name) => loaded.get(base)?.[name];
125
+ if (pick('tools', 'createTools'))
126
+ mod.createTools = pick('tools', 'createTools');
127
+ if (pick('agent-loop', 'runAgentLoop'))
128
+ mod.runAgentLoop = pick('agent-loop', 'runAgentLoop');
129
+ if (pick('system-prompt', 'buildSystemPrompt'))
130
+ mod.buildSystemPrompt = pick('system-prompt', 'buildSystemPrompt');
131
+ if (pick('self-review', 'selfReview'))
132
+ mod.selfReview = pick('self-review', 'selfReview');
133
+ if (pick('self-review', 'selfDiff'))
134
+ mod.selfDiff = pick('self-review', 'selfDiff');
135
+ if (pick('self-review', 'selfApply'))
136
+ mod.selfApply = pick('self-review', 'selfApply');
137
+ if (pick('self-review', 'selfList'))
138
+ mod.selfList = pick('self-review', 'selfList');
139
+ if (pick('web', 'closeWeb'))
140
+ mod.closeWeb = pick('web', 'closeWeb');
141
+ if (pick('spinner', 'createSpinner'))
142
+ mod.createSpinner = pick('spinner', 'createSpinner');
138
143
  return { count: loaded.size, errors };
139
144
  }
140
145
  // Первичная загрузка, чтобы mod.buildSystemPrompt и остальные были заполнены.
@@ -340,7 +345,9 @@ async function promptOnce(question) {
340
345
  inPaste = true;
341
346
  if (before) {
342
347
  for (const ch of before) {
343
- if (ch === '\r' || ch === '\n') { /* внутри вставки — пропускаем */ }
348
+ if (ch === '\r' || ch === '\n') {
349
+ /* внутри вставки — пропускаем */
350
+ }
344
351
  else
345
352
  insertText(ch);
346
353
  }
@@ -447,7 +454,7 @@ async function promptOnce(question) {
447
454
  // в строке спиннера через onChange -> ui.setPending(). Enter отправляет буфер
448
455
  // в onQueue, пустой Enter игнорируется. Поддержаны Backspace, Ctrl+U, Esc-
449
456
  // последовательности (стрелки/Home/End/Delete игнорируются) и bracketed paste.
450
- function watchInput({ onEscape, onChange, onQueue } = {}) {
457
+ function watchInput({ onEscape, onChange, onQueue, } = {}) {
451
458
  const stdin = process.stdin;
452
459
  if (!stdin.isTTY || !stdin.setRawMode)
453
460
  return () => { };
@@ -474,9 +481,12 @@ function watchInput({ onEscape, onChange, onQueue } = {}) {
474
481
  // трактуем как пробелы — сообщение уходит одной строкой.
475
482
  const insert = (text) => {
476
483
  buf += text
477
- .split(CR + LF).join(' ')
478
- .split(CR).join(' ')
479
- .split(LF).join(' ');
484
+ .split(CR + LF)
485
+ .join(' ')
486
+ .split(CR)
487
+ .join(' ')
488
+ .split(LF)
489
+ .join(' ');
480
490
  };
481
491
  function onData(data) {
482
492
  let s = data.toString('utf-8');
@@ -580,7 +590,7 @@ async function resolveWorkdir() {
580
590
  }
581
591
  // ---------- task runner ----------
582
592
  async function runTask(browser, tools, taskText, workdir, opts) {
583
- const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady } = opts;
593
+ const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, } = opts;
584
594
  // В TTY-режиме UI — это LineEditor: он владеет вводом (очередь, Esc,
585
595
  // Ctrl+C) и рисует статус НАД постоянной строкой ввода. В не-TTY режиме
586
596
  // (пайпы) — обычный спиннер + watchInput.
@@ -598,7 +608,8 @@ async function runTask(browser, tools, taskText, workdir, opts) {
598
608
  queue.push(text);
599
609
  ui.setPending(null);
600
610
  ui.stop();
601
- console.log(theme.user('📨 В очередь (' + queue.length + '): ') + theme.assistant(text));
611
+ console.log(theme.user('📨 В очередь (' + queue.length + '): ') +
612
+ theme.assistant(text));
602
613
  ui.thinking();
603
614
  },
604
615
  });
@@ -626,7 +637,7 @@ async function runTask(browser, tools, taskText, workdir, opts) {
626
637
  });
627
638
  if (!queue.length)
628
639
  break;
629
- const queued = queue.shift() ?? "";
640
+ const queued = queue.shift() ?? '';
630
641
  ui.stop();
631
642
  if (editor) {
632
643
  editor.printAbove(theme.user('▶ Из очереди: ') + theme.assistant(queued));
@@ -820,7 +831,8 @@ async function main() {
820
831
  const buildPrompt = () => {
821
832
  let tail;
822
833
  if (reviewMode) {
823
- tail = theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName);
834
+ tail =
835
+ theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName);
824
836
  }
825
837
  else {
826
838
  tail = theme.dir(dirLabel(currentWorkdir));
@@ -865,14 +877,16 @@ async function main() {
865
877
  // оба потока в ed.printAbove.
866
878
  const origLog = console.log.bind(console);
867
879
  const origErr = console.error.bind(console);
868
- const fmt = (a) => typeof a === 'string' ? a : (() => {
869
- try {
870
- return JSON.stringify(a);
871
- }
872
- catch {
873
- return String(a);
874
- }
875
- })();
880
+ const fmt = (a) => typeof a === 'string'
881
+ ? a
882
+ : (() => {
883
+ try {
884
+ return JSON.stringify(a);
885
+ }
886
+ catch {
887
+ return String(a);
888
+ }
889
+ })();
876
890
  console.log = (...a) => ed.printAbove(a.map(fmt).join(' '));
877
891
  console.error = (...a) => ed.printAbove(a.map(fmt).join(' '));
878
892
  // Сохраняем на случай отладки.
@@ -891,7 +905,9 @@ async function main() {
891
905
  let tail;
892
906
  if (reviewMode) {
893
907
  tail =
894
- theme.warn('REVIEW') + theme.dim(':') + theme.dir(reviewMode.snapName);
908
+ theme.warn('REVIEW') +
909
+ theme.dim(':') +
910
+ theme.dir(reviewMode.snapName);
895
911
  }
896
912
  else {
897
913
  tail = theme.dir(dirLabel(currentWorkdir));
@@ -902,7 +918,7 @@ async function main() {
902
918
  catch {
903
919
  break;
904
920
  }
905
- const trimmed = (input || "").trim();
921
+ const trimmed = (input || '').trim();
906
922
  if (!trimmed)
907
923
  continue;
908
924
  const lower = trimmed.toLowerCase();
@@ -1143,7 +1159,9 @@ async function main() {
1143
1159
  saveLastChat(pick.id, currentWorkdir, pick.title);
1144
1160
  transcript.log('resume_chat', { id: pick.id, title: pick.title });
1145
1161
  console.log(theme.assistant(`Чат открыт.`) +
1146
- theme.system(resendPrompt ? ' Системный промпт будет переслан на следующей задаче.\n' : ' Контекст чата сохранён. Системный промпт не пересылается (--resend-prompt чтобы дослать).\n'));
1162
+ theme.system(resendPrompt
1163
+ ? ' Системный промпт будет переслан на следующей задаче.\n'
1164
+ : ' Контекст чата сохранён. Системный промпт не пересылается (--resend-prompt чтобы дослать).\n'));
1147
1165
  }
1148
1166
  catch (e) {
1149
1167
  console.error(theme.error('Не удалось открыть чат:'), e.message);
@@ -1193,7 +1211,8 @@ async function main() {
1193
1211
  sendSystemPromptNext = resendPrompt;
1194
1212
  saveLastChat(id, currentWorkdir);
1195
1213
  transcript.log('resume_chat', { id });
1196
- console.log(theme.assistant('Чат открыт.') + theme.system(String.fromCharCode(10)));
1214
+ console.log(theme.assistant('Чат открыт.') +
1215
+ theme.system(String.fromCharCode(10)));
1197
1216
  }
1198
1217
  catch (e) {
1199
1218
  console.error(theme.error('Не удалось открыть чат:'), e.message);
@@ -9,28 +9,73 @@ import path from 'path';
9
9
  // tool-call с шаблонными строками и экранированными переводами строк в
10
10
  // аргументах доходил до инструментов искажённым. Здесь мы достаём исходный
11
11
  // текст из тела сетевого ответа.
12
- export function extractFromSse(body) {
13
- const chunks = [];
14
- const lines = body.split(String.fromCharCode(10));
15
- for (const line of lines) {
12
+ //
13
+ // Format DeepSeek (SSE, chat.deepseek.com/api/v0/chat/completion):
14
+ // * startovy fragment otveta lezhit v data-chanke s uzlom v.response
15
+ // (v.response.fragments[] -> type RESPONSE -> content);
16
+ // * dalshe tekst dorashchivaetsya chankami vida
17
+ // {"p":"response/fragments/-1/content","o":"APPEND","v":"..."};
18
+ // u samogo pervogo APPEND est p i o, u posleduyushchikh — tolko v;
19
+ // * status generacii prikhodit v {"p":"response/status","o":"SET","v":"..."}
20
+ // i v {"p":"response","o":"BATCH","v":[{...quasi_status...}]}.
21
+ // Reasoning-chanki (thinking) z otvet NE popadayut.
22
+ const NL = String.fromCharCode(10);
23
+ function parseDataLines(body) {
24
+ const out = [];
25
+ for (const line of body.split(NL)) {
16
26
  const trimmed = line.trim();
17
27
  if (!trimmed.startsWith('data:'))
18
28
  continue;
19
29
  const payload = trimmed.slice(5).trim();
20
30
  if (!payload || payload === '[DONE]')
21
31
  continue;
22
- let obj;
23
32
  try {
24
- obj = JSON.parse(payload);
33
+ out.push(JSON.parse(payload));
25
34
  }
26
- catch {
35
+ catch { }
36
+ }
37
+ return out;
38
+ }
39
+ // Собирает текст ответа из SSE-потока. Поддерживает два формата:
40
+ // * OpenAI-совместимый: choices[].delta.content (reasoning_content
41
+ // игнорируется — это размышления, а не ответ);
42
+ // * DeepSeek chat.deepseek.com: стартовый v.response.fragments[] и
43
+ // инкрементальные APPEND-чанки response/fragments/-1/content.
44
+ export function extractFromSse(body) {
45
+ let out = '';
46
+ for (const obj of parseDataLines(body)) {
47
+ if (obj == null || typeof obj !== 'object')
48
+ continue;
49
+ const o = obj;
50
+ // OpenAI-sovmestimyj format: choices[].delta.content.
51
+ if (Array.isArray(o.choices)) {
52
+ out += pickAnswerText(o);
27
53
  continue;
28
54
  }
29
- const t = pickAnswerText(obj);
30
- if (t)
31
- chunks.push(t);
55
+ // DeepSeek: startovy snapshot otveta v.response.fragments[].
56
+ const v = o.v;
57
+ const resp = v && typeof v === 'object'
58
+ ? v.response
59
+ : undefined;
60
+ if (resp && Array.isArray(resp.fragments)) {
61
+ for (const fr of resp.fragments) {
62
+ if (fr && fr.type === 'RESPONSE' && typeof fr.content === 'string') {
63
+ out += fr.content;
64
+ }
65
+ }
66
+ continue;
67
+ }
68
+ // Инкрементальный APPEND: p='response/fragments/-1/content' -> v=строка;
69
+ // последующие чанки идут с одним полем v.
70
+ if (o.p === 'response/fragments/-1/content' && typeof o.v === 'string') {
71
+ out += o.v;
72
+ continue;
73
+ }
74
+ if (o.p === undefined && typeof o.v === 'string') {
75
+ out += o.v;
76
+ }
32
77
  }
33
- return chunks.join('');
78
+ return out;
34
79
  }
35
80
  export function extractFromJson(body) {
36
81
  let obj;
@@ -42,9 +87,9 @@ export function extractFromJson(body) {
42
87
  }
43
88
  return pickAnswerText(obj);
44
89
  }
45
- // Достаёт финальный текст ответа из узла, НЕ смешивая его с reasoning.
90
+ // Достает финальный текст ответа из узла, НЕ смешивая его с reasoning.
46
91
  // Порядок: choices/messages -> delta/message -> content; reasoning_content
47
- // намеренно не берём — это размышления модели, а не ответ.
92
+ // намеренно не берем — это размышления модели, а не ответ.
48
93
  function pickAnswerText(node) {
49
94
  if (node == null)
50
95
  return '';
@@ -87,7 +132,7 @@ export function extractAnswer(body) {
87
132
  return extractFromJson(body);
88
133
  }
89
134
  // Сохраняет тело сетевого ответа DeepSeek на диск для разбора постфактум.
90
- // Файлы лежат в ~/.zames/net-log — по ним видно реальный формат ответа.
135
+ // Файлы лежат в ~/.zames/net-log — po nim видно реальный формат ответа.
91
136
  export async function dumpNetBody(url, body) {
92
137
  try {
93
138
  const dir = path.join(os.homedir(), '.zames', 'net-log');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {