zames_pro 2.4.4 → 2.4.6
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 -47
- package/dist/browser.js +56 -42
- package/dist/config.js +1 -1
- package/package.json +1 -1
package/dist/agent-loop.js
CHANGED
|
@@ -6,9 +6,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
6
6
|
await browser.newChat();
|
|
7
7
|
transcript?.log('new_chat');
|
|
8
8
|
}
|
|
9
|
-
// Сообщаем вызывающему актуальный chat id.
|
|
10
|
-
// (он появляется только после первой отправки), поэтому зовём колбэк и
|
|
11
|
-
// здесь, и после первой реальной отправки ниже.
|
|
9
|
+
// Сообщаем вызывающему актуальный chat id.
|
|
12
10
|
let lastReportedChatId = null;
|
|
13
11
|
const reportChat = async () => {
|
|
14
12
|
let id = null;
|
|
@@ -43,29 +41,26 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
43
41
|
gitContext: gitText,
|
|
44
42
|
});
|
|
45
43
|
onThinking();
|
|
46
|
-
|
|
44
|
+
// system-prompt — отправка агента: с паузой (agent: true).
|
|
45
|
+
await browser.ask(systemPrompt, { timeout: 60_000, agent: true });
|
|
47
46
|
await reportChat();
|
|
48
47
|
}
|
|
49
48
|
let message = task;
|
|
50
49
|
transcript?.log('task', { task });
|
|
51
|
-
// Счётчик «ответ похож на tool-call, но не распознан». Чтобы модель,
|
|
52
|
-
// написавшая битый JSON/XML, не останавливала агента молча, мы просим
|
|
53
|
-
// её переотправить вызов. Ограничиваем число таких попыток.
|
|
54
50
|
let malformedRetries = 0;
|
|
55
51
|
const MAX_MALFORMED_RETRIES = 3;
|
|
56
|
-
// Счётчик «пустой/служебный ответ без вызова инструмента». Модель иногда
|
|
57
|
-
// отвечает статусом интерфейса (Reading…), тостом лимита или пустой
|
|
58
|
-
// строкой. Это НЕ финальный ответ — просим её продолжить, а не молча
|
|
59
|
-
// останавливаемся (раньше агент на этом вставал).
|
|
60
52
|
let stallRetries = 0;
|
|
61
53
|
const MAX_STALL_RETRIES = 5;
|
|
62
54
|
for (let i = 0; i < maxIterations; i++) {
|
|
63
55
|
onThinking();
|
|
64
|
-
|
|
56
|
+
// Первое сообщение (task) — пользовательский ввод: без паузы.
|
|
57
|
+
// Последующие (tool-result и просьбы переотправить) — агентские:
|
|
58
|
+
// с паузой, чтобы не упираться в лимит частоты.
|
|
59
|
+
const isFirst = i === 0;
|
|
60
|
+
const rawResponse = await browser.ask(message, { agent: !isFirst });
|
|
65
61
|
await reportChat();
|
|
66
62
|
transcript?.log('assistant_raw', { response: rawResponse });
|
|
67
|
-
// Пользователь прервал генерацию (Esc/Ctrl+C)
|
|
68
|
-
// модели и не запускаем инструменты — корректно завершаем задачу.
|
|
63
|
+
// Пользователь прервал генерацию (Esc/Ctrl+C).
|
|
69
64
|
if (/^\s*\(прервано пользователем\)\s*$/.test(rawResponse)) {
|
|
70
65
|
transcript?.log('user_aborted');
|
|
71
66
|
return rawResponse;
|
|
@@ -78,17 +73,12 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
78
73
|
}
|
|
79
74
|
const parsedCalls = Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
|
|
80
75
|
if (parsedCalls.some((p) => p && p._permissive)) {
|
|
81
|
-
// Пишем в транскрипт (для отладки), но не сыпем в консоль.
|
|
82
76
|
transcript?.log('permissive_parse', { response: rawResponse });
|
|
83
77
|
if (debugLog) {
|
|
84
78
|
console.error('внимание: tool-call распознан нестрогим парсером');
|
|
85
79
|
}
|
|
86
80
|
}
|
|
87
81
|
if (!parsed) {
|
|
88
|
-
// Ответ не распознан как tool-call. Если он ПОХОЖ на попытку вызова
|
|
89
|
-
// (есть tool/invoke/parameter, но JSON/XML битый) — это почти всегда
|
|
90
|
-
// ошибка формата. Не считаем её финальным ответом (иначе агент молча
|
|
91
|
-
// остановится), а просим модель переотправить вызов корректно.
|
|
92
82
|
const looksLikeToolCall = /("tool"\s*:|\btool_calls?\b|\binvoke\b|\bparameter\b|DSML|function_call)/i.test(rawResponse) || /\{\s*"?(tool|name)"?\s*:/.test(rawResponse);
|
|
93
83
|
if (looksLikeToolCall && malformedRetries < MAX_MALFORMED_RETRIES) {
|
|
94
84
|
malformedRetries++;
|
|
@@ -110,9 +100,6 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
110
100
|
'Например: {"tool": "Read", "args": {"path": "src/index.js"}}';
|
|
111
101
|
continue;
|
|
112
102
|
}
|
|
113
|
-
// Пустой или служебный ответ (статус интерфейса, тост лимита, пробелы)
|
|
114
|
-
// не считаем финальным: просим модель продолжить. Иначе агент
|
|
115
|
-
// останавливается, хотя должен был вызвать инструмент.
|
|
116
103
|
const trimmed = (rawResponse || '').trim();
|
|
117
104
|
const looksService = !trimmed ||
|
|
118
105
|
trimmed.length < 2 ||
|
|
@@ -272,7 +259,6 @@ function tryParseArray(str) {
|
|
|
272
259
|
return null;
|
|
273
260
|
}
|
|
274
261
|
}
|
|
275
|
-
// Жадный разбор args для грязного JSON (незаэкранированные кавычки в строке).
|
|
276
262
|
function parseArgsGreedy(str) {
|
|
277
263
|
const result = {};
|
|
278
264
|
let i = str.indexOf('{') + 1;
|
|
@@ -416,8 +402,6 @@ function parseArgsPermissive(str) {
|
|
|
416
402
|
i = j + 1;
|
|
417
403
|
}
|
|
418
404
|
else if (str[i] === '{' || str[i] === '[') {
|
|
419
|
-
// Вложенный объект/массив: находим сбалансированный фрагмент и
|
|
420
|
-
// пытаемся распарсить его как JSON (или как вложенный permissive).
|
|
421
405
|
const open = str[i];
|
|
422
406
|
const close = open === '{' ? '}' : ']';
|
|
423
407
|
const end = findMatching(str, i, open, close);
|
|
@@ -522,15 +506,8 @@ function parseToolCallPermissive(text) {
|
|
|
522
506
|
const openIdx = text.indexOf('{', argsIdx);
|
|
523
507
|
if (openIdx === -1)
|
|
524
508
|
return null;
|
|
525
|
-
// Берём сбалансированный по скобкам фрагмент args (с учётом строк),
|
|
526
|
-
// а не первый попавшийся '}'. Иначе вложенные объекты/массивы или
|
|
527
|
-
// фигурные скобки внутри строк ломают разбор.
|
|
528
509
|
const endIdx = findMatching(text, openIdx, '{', '}');
|
|
529
510
|
if (tool === 'Edit') {
|
|
530
|
-
// Edit-аргументы часто содержат сырые кавычки и переводы строк, из-за
|
|
531
|
-
// чего балансировка скобок сбоит. parseEditArgs рассчитан ровно на
|
|
532
|
-
// такой случай, поэтому пробуем его и на «хвосте» до конца текста,
|
|
533
|
-
// а не только на сбалансированном фрагменте.
|
|
534
511
|
const balanced = endIdx === -1 ? null : text.slice(openIdx, endIdx + 1);
|
|
535
512
|
const tailToEnd = text.slice(openIdx);
|
|
536
513
|
for (const frag of [balanced, tailToEnd]) {
|
|
@@ -541,9 +518,6 @@ function parseToolCallPermissive(text) {
|
|
|
541
518
|
return { tool, args: e };
|
|
542
519
|
}
|
|
543
520
|
}
|
|
544
|
-
// Балансировка скобок сбоит, если в строковых значениях есть сырые
|
|
545
|
-
// кавычки/скобки (частый случай для Bash/Write с кодом внутри). Тогда
|
|
546
|
-
// пробуем разобрать args из «хвоста» до конца текста и жадным парсером.
|
|
547
521
|
const frags = [];
|
|
548
522
|
if (endIdx !== -1)
|
|
549
523
|
frags.push(text.slice(openIdx, endIdx + 1));
|
|
@@ -558,8 +532,6 @@ function parseToolCallPermissive(text) {
|
|
|
558
532
|
}
|
|
559
533
|
return null;
|
|
560
534
|
}
|
|
561
|
-
// Специализированный разбор args для Edit: ровно три поля path/old_string/
|
|
562
|
-
// new_string, значения могут содержать сырые кавычки и переводы строк.
|
|
563
535
|
function parseEditArgs(str) {
|
|
564
536
|
const keyRe = (name) => new RegExp('"' + name + '"\\s*:\\s*"');
|
|
565
537
|
const readValue = (name, nextNames) => {
|
|
@@ -665,9 +637,6 @@ export function parseToolCall(text) {
|
|
|
665
637
|
const second = tryParse(repaired);
|
|
666
638
|
if (second)
|
|
667
639
|
return second;
|
|
668
|
-
// Сырые переводы строк/табы внутри строковых значений (old_string,
|
|
669
|
-
// new_string, content) делают JSON невалидным. Экранируем их и
|
|
670
|
-
// пробуем снова, иначе многострочный вызов не распознаётся.
|
|
671
640
|
const ctrl = repairRawControlChars(raw);
|
|
672
641
|
const third = tryParse(ctrl);
|
|
673
642
|
if (third)
|
|
@@ -680,22 +649,15 @@ export function parseToolCall(text) {
|
|
|
680
649
|
parseToolCallPermissive(repairRawControlChars(cleaned));
|
|
681
650
|
if (permissive)
|
|
682
651
|
return { ...permissive, _permissive: true };
|
|
683
|
-
// Fallback: модель могла обернуть JSON в прозу и/или добавить мусорные
|
|
684
|
-
// теги после него. Отрезаем всё до первого '"tool"' и всё, что похоже
|
|
685
|
-
// на XML/DSML-хвост, затем пробуем permissive-разбор ещё раз.
|
|
686
652
|
const toolIdx = cleaned.search(/["']?tool["']?\s:/);
|
|
687
653
|
if (toolIdx > 0) {
|
|
688
654
|
let tail = cleaned.slice(toolIdx);
|
|
689
|
-
// Регексп обязан матчить многосимвольные теги (<|DSML|invoke ...>),
|
|
690
|
-
// поэтому <[^>]*>, а не <[^>]> — иначе DSML-хвост не срезается,
|
|
691
|
-
// permissive-разбор падает и агент молча останавливается.
|
|
692
655
|
tail = tail.replace(/<[^>]*>.*$/s, '').trim();
|
|
693
656
|
const tailPermissive = parseToolCallPermissive('{"' + tail) ||
|
|
694
657
|
parseToolCallPermissive('{"' + repairRawControlChars(tail));
|
|
695
658
|
if (tailPermissive)
|
|
696
659
|
return { ...tailPermissive, _permissive: true };
|
|
697
660
|
}
|
|
698
|
-
// Последний fallback: модель ответила XML/DSML-блоком вместо JSON.
|
|
699
661
|
const xmlCalls = parseXmlToolCalls(cleaned);
|
|
700
662
|
if (xmlCalls)
|
|
701
663
|
return Array.isArray(xmlCalls) ? xmlCalls : [xmlCalls];
|
package/dist/browser.js
CHANGED
|
@@ -39,8 +39,7 @@ const STOP_SELECTORS = [
|
|
|
39
39
|
// Служебные статусы интерфейса DeepSeek, которые НЕ являются ответом модели.
|
|
40
40
|
// Иначе агент принимает статус (Reading...) за ответ и ломает разбор.
|
|
41
41
|
const STATUS_RE = /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i;
|
|
42
|
-
// Ответ DeepSeek при превышении лимита частоты.
|
|
43
|
-
// отправлено — нужно подождать и повторить, а не считать это ответом модели.
|
|
42
|
+
// Ответ DeepSeek при превышении лимита частоты.
|
|
44
43
|
const RATE_LIMIT_RE = /(messages? too frequent|too many requests|rate limit|слишком часто|повторите позже|try again later)/i;
|
|
45
44
|
export function isRateLimitText(text) {
|
|
46
45
|
return RATE_LIMIT_RE.test(String(text || ''));
|
|
@@ -113,7 +112,7 @@ export class DeepSeekBrowser {
|
|
|
113
112
|
_netSniff;
|
|
114
113
|
_netSniffLimit;
|
|
115
114
|
_netHookInstalled;
|
|
116
|
-
constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 3, stabilityDelayMs = 1000, minSendIntervalMs =
|
|
115
|
+
constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 3, stabilityDelayMs = 1000, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, } = {}) {
|
|
117
116
|
this.headless = headless;
|
|
118
117
|
this.debug = debug;
|
|
119
118
|
this.channel = channel;
|
|
@@ -305,11 +304,31 @@ export class DeepSeekBrowser {
|
|
|
305
304
|
return out.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim();
|
|
306
305
|
}, ANSWER_SELECTORS);
|
|
307
306
|
}
|
|
308
|
-
//
|
|
309
|
-
//
|
|
307
|
+
// Читаем ТОЛЬКО видимые тосты/уведомления/ошибки, а не весь текст
|
|
308
|
+
// страницы. Иначе ловим «try again later» из служебных/скрытых блоков
|
|
309
|
+
// и уходим в ложное ожидание лимита на 5 минут.
|
|
310
310
|
async _readPageText() {
|
|
311
311
|
return await this.page
|
|
312
|
-
.evaluate(() =>
|
|
312
|
+
.evaluate(() => {
|
|
313
|
+
const sels = [
|
|
314
|
+
'[role="alert"]',
|
|
315
|
+
'[class*="toast" i]',
|
|
316
|
+
'[class*="notification" i]',
|
|
317
|
+
'[class*="alert" i]',
|
|
318
|
+
'[class*="error" i]',
|
|
319
|
+
];
|
|
320
|
+
let out = '';
|
|
321
|
+
for (const s of sels) {
|
|
322
|
+
for (const el of Array.from(document.querySelectorAll(s))) {
|
|
323
|
+
const e = el;
|
|
324
|
+
const st = getComputedStyle(e);
|
|
325
|
+
if (st.display === 'none' || st.visibility === 'hidden')
|
|
326
|
+
continue;
|
|
327
|
+
out += ' ' + (e.innerText || '');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return out;
|
|
331
|
+
})
|
|
313
332
|
.catch(() => '');
|
|
314
333
|
}
|
|
315
334
|
async _readLastAnswerTextClean() {
|
|
@@ -323,10 +342,7 @@ export class DeepSeekBrowser {
|
|
|
323
342
|
}
|
|
324
343
|
// Найти кнопку Stop в интерфейсе DeepSeek. Полагаться только на класс
|
|
325
344
|
// нельзя: во время генерации кнопка отправки (та же circle-кнопка)
|
|
326
|
-
// меняет иконку на «квадрат» (stop), сохраняя классы.
|
|
327
|
-
// 1) проверяем явные селекторы (aria-label/текст Stop);
|
|
328
|
-
// 2) ищем primary circle-кнопку и смотрим на её иконку: rect/квадрат =
|
|
329
|
-
// stop, path/стрелка = send.
|
|
345
|
+
// меняет иконку на «квадрат» (stop), сохраняя классы.
|
|
330
346
|
async _stopButtonVisible() {
|
|
331
347
|
const explicit = await this._findVisible(STOP_SELECTORS, 250);
|
|
332
348
|
if (explicit)
|
|
@@ -360,7 +376,6 @@ export class DeepSeekBrowser {
|
|
|
360
376
|
async stopGeneration() {
|
|
361
377
|
this._abort = true;
|
|
362
378
|
this._stopped = true;
|
|
363
|
-
// 1) Пробуем явные селекторы Stop.
|
|
364
379
|
const btn = await this._findVisible(STOP_SELECTORS, 500);
|
|
365
380
|
if (btn) {
|
|
366
381
|
try {
|
|
@@ -369,7 +384,6 @@ export class DeepSeekBrowser {
|
|
|
369
384
|
}
|
|
370
385
|
catch { }
|
|
371
386
|
}
|
|
372
|
-
// 2) Кликаем по primary circle-кнопке, если у неё иконка-квадрат.
|
|
373
387
|
const clicked = await this.page
|
|
374
388
|
.evaluate(() => {
|
|
375
389
|
const btns = Array.from(document.querySelectorAll('div[role="button"], button'));
|
|
@@ -388,7 +402,6 @@ export class DeepSeekBrowser {
|
|
|
388
402
|
.catch(() => false);
|
|
389
403
|
if (clicked)
|
|
390
404
|
return true;
|
|
391
|
-
// 3) Крайний случай — нажать Esc в поле ввода (иногда останавливает).
|
|
392
405
|
try {
|
|
393
406
|
await this.page.keyboard.press('Escape');
|
|
394
407
|
return true;
|
|
@@ -396,22 +409,21 @@ export class DeepSeekBrowser {
|
|
|
396
409
|
catch { }
|
|
397
410
|
return false;
|
|
398
411
|
}
|
|
399
|
-
async ask(prompt, { timeout = this.answerTimeoutMs } = {}) {
|
|
412
|
+
async ask(prompt, { timeout = this.answerTimeoutMs, agent = false, } = {}) {
|
|
400
413
|
let lastErr = null;
|
|
401
414
|
let attempt = 0;
|
|
402
415
|
let rateLimitRetries = 0;
|
|
403
416
|
while (attempt < this.askRetries) {
|
|
404
417
|
attempt++;
|
|
405
418
|
try {
|
|
406
|
-
return await this._askOnce(prompt, { timeout });
|
|
419
|
+
return await this._askOnce(prompt, { timeout, agent });
|
|
407
420
|
}
|
|
408
421
|
catch (e) {
|
|
409
422
|
lastErr = e;
|
|
410
|
-
// Лимит частоты: DeepSeek не принял сообщение. Ждём долго
|
|
411
|
-
//
|
|
412
|
-
//
|
|
423
|
+
// Лимит частоты: DeepSeek не принял сообщение. Ждём долго и
|
|
424
|
+
// повторяем отправку в ТОТ ЖЕ чат (без newChat — иначе теряется
|
|
425
|
+
// контекст). Паузы не расходуют обычные попытки ask().
|
|
413
426
|
if (e instanceof RateLimitError) {
|
|
414
|
-
attempt--;
|
|
415
427
|
rateLimitRetries++;
|
|
416
428
|
if (rateLimitRetries > this.maxRateLimitRetries) {
|
|
417
429
|
console.error(theme.error(`✖ DeepSeek не принял сообщение после ${rateLimitRetries} пауз по ${Math.ceil(this.rateLimitWaitMs / 60000)} мин.`));
|
|
@@ -419,7 +431,6 @@ export class DeepSeekBrowser {
|
|
|
419
431
|
}
|
|
420
432
|
console.error(theme.warn(`⏳ DeepSeek: «слишком часто». Жду ${Math.ceil(this.rateLimitWaitMs / 60000)} мин (${rateLimitRetries}/${this.maxRateLimitRetries}) и повторю...`));
|
|
421
433
|
await this.page.waitForTimeout(this.rateLimitWaitMs);
|
|
422
|
-
await this.newChat().catch(() => { });
|
|
423
434
|
continue;
|
|
424
435
|
}
|
|
425
436
|
console.error(`\n⚠ ask() попытка ${attempt}/${this.askRetries} провалилась: ${e.message}`);
|
|
@@ -484,11 +495,12 @@ export class DeepSeekBrowser {
|
|
|
484
495
|
await this.page.keyboard.insertText(text);
|
|
485
496
|
}
|
|
486
497
|
}
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
|
|
491
|
-
|
|
498
|
+
// Пауза между отправками. Применяется ТОЛЬКО к сообщениям агента
|
|
499
|
+
// (tool-result, system-prompt), чтобы не упираться в лимит частоты.
|
|
500
|
+
// Пользовательский ввод отправляется без задержки.
|
|
501
|
+
async _waitForSendSlot(agent) {
|
|
502
|
+
if (!agent)
|
|
503
|
+
return;
|
|
492
504
|
if (!this._lastSentAt)
|
|
493
505
|
return;
|
|
494
506
|
const gap = this.minSendIntervalMs - (Date.now() - this._lastSentAt);
|
|
@@ -497,17 +509,15 @@ export class DeepSeekBrowser {
|
|
|
497
509
|
console.error(theme.warn(`⏳ пауза ${Math.ceil(gap / 1000)}с перед отправкой...`));
|
|
498
510
|
await this.page.waitForTimeout(gap);
|
|
499
511
|
}
|
|
500
|
-
async _askOnce(prompt, { timeout }) {
|
|
501
|
-
// Сбрасываем флаг прерывания ТОЛЬКО в самом начале отправки.
|
|
502
|
-
// сбрасывался перед циклом ожидания — и Esc/Ctrl+C, нажатые во время
|
|
503
|
-
// старта генерации, терялись, запрос не останавливался.
|
|
512
|
+
async _askOnce(prompt, { timeout, agent }) {
|
|
513
|
+
// Сбрасываем флаг прерывания ТОЛЬКО в самом начале отправки.
|
|
504
514
|
this._abort = false;
|
|
505
515
|
const input = await this._findVisible(INPUT_SELECTORS, 10_000);
|
|
506
516
|
if (!input) {
|
|
507
517
|
throw new Error('Не найдено поле ввода. Запустите /debug-dom и поправьте INPUT_SELECTORS.');
|
|
508
518
|
}
|
|
509
519
|
const beforeText = await this._readLastAnswerTextClean().catch(() => '');
|
|
510
|
-
await this._waitForSendSlot();
|
|
520
|
+
await this._waitForSendSlot(agent);
|
|
511
521
|
this._netCapture = '';
|
|
512
522
|
this._netCaptureAt = 0;
|
|
513
523
|
await this._setInputText(input, prompt);
|
|
@@ -532,15 +542,13 @@ export class DeepSeekBrowser {
|
|
|
532
542
|
this._lastSentAt = Date.now();
|
|
533
543
|
// Ждём старта: либо появился Stop, либо изменился текст ответа,
|
|
534
544
|
// либо вырос общий объём текста на странице. Параллельно ловим
|
|
535
|
-
// тост о превышении лимита частоты
|
|
536
|
-
// приёма сообщения, и его надо обработать паузой и повтором.
|
|
545
|
+
// тост о превышении лимита частоты (только тосты, не весь body).
|
|
537
546
|
const startDeadline = Date.now() + 15_000;
|
|
538
547
|
const startBodyLen = await this.page
|
|
539
548
|
.evaluate(() => document.body.innerText.length)
|
|
540
549
|
.catch(() => 0);
|
|
541
550
|
let started = false;
|
|
542
551
|
while (Date.now() < startDeadline) {
|
|
543
|
-
// Прервали (Esc/Ctrl+C) ещё до старта генерации — выходим сразу.
|
|
544
552
|
if (this._abort)
|
|
545
553
|
return '(прервано пользователем)';
|
|
546
554
|
const pageText = await this._readPageText();
|
|
@@ -559,25 +567,31 @@ export class DeepSeekBrowser {
|
|
|
559
567
|
await this.page.waitForTimeout(300);
|
|
560
568
|
}
|
|
561
569
|
if (!started) {
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
|
|
565
|
-
if (isRateLimitText(pageText)) {
|
|
566
|
-
throw new RateLimitError(pageText.slice(0, 300));
|
|
567
|
-
}
|
|
570
|
+
// Больше НЕ проверяем лимит по всему тексту страницы — это давало
|
|
571
|
+
// ложные срабатывания и 5-минутные паузы. Просто сообщаем, что
|
|
572
|
+
// генерация не началась.
|
|
568
573
|
throw new Error('Ответ не начал генерироваться за 15с. Возможно, сообщение не отправилось.');
|
|
569
574
|
}
|
|
570
575
|
// Ждём, пока ответ перестанет меняться. Условие "не генерируется"
|
|
571
|
-
// проверяем через рост текста, а НЕ через _isGenerating()
|
|
572
|
-
//
|
|
573
|
-
//
|
|
576
|
+
// проверяем через рост текста, а НЕ через _isGenerating().
|
|
577
|
+
// Параллельно ловим тост лимита частоты, если он всплывёт во время
|
|
578
|
+
// генерации (читаем только тосты, ложных срабатываний нет).
|
|
574
579
|
const deadline = Date.now() + timeout;
|
|
575
580
|
let last = '';
|
|
576
581
|
let stable = 0;
|
|
582
|
+
let tick = 0;
|
|
577
583
|
while (Date.now() < deadline) {
|
|
578
584
|
if (this._abort) {
|
|
579
585
|
return last || '(прервано пользователем)';
|
|
580
586
|
}
|
|
587
|
+
// Тост лимита проверяем не каждый тик, а раз в ~5 тиков, чтобы не
|
|
588
|
+
// дёргать DOM лишний раз.
|
|
589
|
+
if (tick++ % 5 === 0) {
|
|
590
|
+
const pageText = await this._readPageText();
|
|
591
|
+
if (isRateLimitText(pageText)) {
|
|
592
|
+
throw new RateLimitError(pageText.slice(0, 300));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
581
595
|
const cur = await this._readLastAnswerTextClean().catch(() => '');
|
|
582
596
|
if (cur && cur === last) {
|
|
583
597
|
stable++;
|
package/dist/config.js
CHANGED