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.
- package/dist/agent-loop.js +9 -2
- package/dist/browser.js +53 -122
- package/dist/index.js +74 -55
- package/dist/net-capture.js +59 -14
- package/package.json +1 -1
package/dist/agent-loop.js
CHANGED
|
@@ -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(
|
|
83
|
+
transcript?.log('malformed_toolcall', {
|
|
84
|
+
attempt: malformedRetries,
|
|
85
|
+
response: rawResponse,
|
|
86
|
+
});
|
|
84
87
|
if (debugLog) {
|
|
85
|
-
console.error(
|
|
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
|
|
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' ||
|
|
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 || '')
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
'
|
|
81
|
-
'
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
'
|
|
87
|
-
'
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
'
|
|
91
|
-
'
|
|
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
|
|
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(
|
|
118
|
+
loaded.set(base, m);
|
|
114
119
|
}
|
|
115
120
|
catch (e) {
|
|
116
|
-
errors.push(
|
|
121
|
+
errors.push(rel + ': ' + e.message);
|
|
117
122
|
}
|
|
118
123
|
}
|
|
119
|
-
const pick = (
|
|
120
|
-
if (pick('
|
|
121
|
-
mod.createTools = pick('
|
|
122
|
-
if (pick('
|
|
123
|
-
mod.runAgentLoop = pick('
|
|
124
|
-
if (pick('
|
|
125
|
-
mod.buildSystemPrompt = pick('
|
|
126
|
-
if (pick('
|
|
127
|
-
mod.selfReview = pick('
|
|
128
|
-
if (pick('
|
|
129
|
-
mod.selfDiff = pick('
|
|
130
|
-
if (pick('
|
|
131
|
-
mod.selfApply = pick('
|
|
132
|
-
if (pick('
|
|
133
|
-
mod.selfList = pick('
|
|
134
|
-
if (pick('
|
|
135
|
-
mod.closeWeb = pick('
|
|
136
|
-
if (pick('
|
|
137
|
-
mod.createSpinner = pick('
|
|
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)
|
|
478
|
-
.
|
|
479
|
-
.split(
|
|
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 + '): ') +
|
|
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 =
|
|
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'
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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') +
|
|
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 ||
|
|
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
|
|
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('Чат открыт.') +
|
|
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);
|
package/dist/net-capture.js
CHANGED
|
@@ -9,28 +9,73 @@ import path from 'path';
|
|
|
9
9
|
// tool-call с шаблонными строками и экранированными переводами строк в
|
|
10
10
|
// аргументах доходил до инструментов искажённым. Здесь мы достаём исходный
|
|
11
11
|
// текст из тела сетевого ответа.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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
|
-
//
|
|
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');
|