zames_pro 2.29.16 → 2.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-loop.js +138 -12
- package/dist/browser.js +422 -145
- package/dist/commands.js +78 -0
- package/dist/compact.js +178 -0
- package/dist/config-menu.js +22 -5
- package/dist/config.js +43 -0
- package/dist/confirm.js +30 -6
- package/dist/context.js +3 -2
- package/dist/gitTools.js +3 -2
- package/dist/i18n.js +94 -8
- package/dist/index.js +222 -174
- package/dist/input.js +104 -11
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -160,6 +160,10 @@ const STOP_SELECTORS = [
|
|
|
160
160
|
'button:has-text("Остановить")',
|
|
161
161
|
'button[aria-label*="Stop" i]',
|
|
162
162
|
];
|
|
163
|
+
// Accessible name of the Stop button, for a TRUSTED Playwright click. It is a
|
|
164
|
+
// bare stop word (optionally with a short suffix), anchored and length-limited
|
|
165
|
+
// so a "Stop" inside rendered prose is never clicked.
|
|
166
|
+
export const STOP_NAME_RE = /^(?:stop|остановить)(?:\s+\S{0,20})?\s*[.!…]?$/i;
|
|
163
167
|
// DeepSeek's "Continue" button. With the reasoning ("Deep thinking") toggle
|
|
164
168
|
// on, the server caps the THINK phase: the reasoning stops mid-way and the UI
|
|
165
169
|
// offers a Continue button (this is NOT the same as a truncated turn with
|
|
@@ -332,6 +336,17 @@ export class DeepSeekBrowser {
|
|
|
332
336
|
// rate limit. The click path now respects `minSendIntervalMs` like a send.
|
|
333
337
|
_lastContinueAt;
|
|
334
338
|
_abort;
|
|
339
|
+
// Set by cancelPendingAsk(): the agent loop's watchdog raced ask() against a
|
|
340
|
+
// deadline and the timer won. The underlying ask() may still be inside a
|
|
341
|
+
// rate-limit wait or the finish loop; this flag makes it bail out so the
|
|
342
|
+
// next iteration does not run TWO asks against the same page (two sends /
|
|
343
|
+
// two Continue clicks). Reset at the start of every _askOnce.
|
|
344
|
+
_askCancelled;
|
|
345
|
+
// Number of ask() calls currently in flight from OUR code. A
|
|
346
|
+
// completion/continue body that arrives while this is 0 was NOT initiated
|
|
347
|
+
// by us — it is the operator's own turn in the chat (or a manual resume in
|
|
348
|
+
// the web UI), and our next agent send must wait out the throttle from it.
|
|
349
|
+
_askInFlight;
|
|
335
350
|
// The user pressed Esc/Ctrl+C — a "stop" for the WHOLE current batch of
|
|
336
351
|
// tasks (including the queue). Unlike _abort (reset on every send), this
|
|
337
352
|
// flag lives until a new task is explicitly started from the prompt.
|
|
@@ -344,6 +359,13 @@ export class DeepSeekBrowser {
|
|
|
344
359
|
// (only reasoning). The DOM keeps showing reasoning/old text, so the loop
|
|
345
360
|
// must not wait it out — it clicks Continue / resends. Reset before a send.
|
|
346
361
|
_netNoAnswer;
|
|
362
|
+
// True when the LAST completion/continue body carried the rate limit
|
|
363
|
+
// ("Messages too frequent", finish_reason rate_limit_reached). The toast
|
|
364
|
+
// may be missed (it is a short-lived DOM node), so we ALSO recognise the
|
|
365
|
+
// error from the SSE body — otherwise ask() waited out the deadline and
|
|
366
|
+
// threw ds.send_no_new_answer while the chat plainly showed the rate limit.
|
|
367
|
+
// Reset before a send.
|
|
368
|
+
_netRateLimited;
|
|
347
369
|
_netChatId;
|
|
348
370
|
// The latest CONTEXT size (in tokens) DeepSeek reported in the current
|
|
349
371
|
// chat. It is the `accumulated_token_usage` counter taken from the SSE
|
|
@@ -361,8 +383,6 @@ export class DeepSeekBrowser {
|
|
|
361
383
|
// Last error from fetchChatMessages ('' on success). Surfaced under --debug
|
|
362
384
|
// so a missing restored history can be diagnosed.
|
|
363
385
|
_lastHistoryError;
|
|
364
|
-
_netSniff;
|
|
365
|
-
_netSniffLimit;
|
|
366
386
|
// The RAW SSE body of the LAST answer response. Unlike `_netCapture`, this
|
|
367
387
|
// is kept even when the body carries no RESPONSE text (a truncated turn:
|
|
368
388
|
// generation_err / INCOMPLETE, or FINISHED-with-only-reasoning). The
|
|
@@ -421,18 +441,19 @@ export class DeepSeekBrowser {
|
|
|
421
441
|
this._lastSentAt = 0;
|
|
422
442
|
this._lastContinueAt = 0;
|
|
423
443
|
this._abort = false;
|
|
444
|
+
this._askCancelled = false;
|
|
445
|
+
this._askInFlight = 0;
|
|
424
446
|
this._stopped = false;
|
|
425
447
|
this._netCapture = '';
|
|
426
448
|
this._netCaptureAt = 0;
|
|
427
449
|
this._netNoAnswer = false;
|
|
450
|
+
this._netRateLimited = false;
|
|
428
451
|
this._netChatId = null;
|
|
429
452
|
this._lastTokenUsage = null;
|
|
430
453
|
this._toggles = { deepThinking, webSearch };
|
|
431
454
|
this._apiAuth = '';
|
|
432
455
|
this._apiPow = '';
|
|
433
456
|
this._lastHistoryError = '';
|
|
434
|
-
this._netSniff = [];
|
|
435
|
-
this._netSniffLimit = 5;
|
|
436
457
|
this._netBody = '';
|
|
437
458
|
this._netHookInstalled = false;
|
|
438
459
|
this.onSendStart = null;
|
|
@@ -590,9 +611,6 @@ export class DeepSeekBrowser {
|
|
|
590
611
|
const body = await resp.text().catch(() => '');
|
|
591
612
|
if (!body)
|
|
592
613
|
return;
|
|
593
|
-
this._netSniff.push({ url, contentType: ct, body });
|
|
594
|
-
if (this._netSniff.length > this._netSniffLimit)
|
|
595
|
-
this._netSniff.shift();
|
|
596
614
|
void dumpNetBody(url, body);
|
|
597
615
|
// Context size (tokens) reported by DeepSeek for this answer. Kept
|
|
598
616
|
// even when extractAnswer() returns nothing (a history_messages
|
|
@@ -600,8 +618,13 @@ export class DeepSeekBrowser {
|
|
|
600
618
|
const usage = extractTokenUsage(body);
|
|
601
619
|
if (usage !== null)
|
|
602
620
|
this._lastTokenUsage = usage;
|
|
621
|
+
// Only a completion/continue turn carries a real answer. Gate the
|
|
622
|
+
// capture assignment to those URLs: a future endpoint returning a
|
|
623
|
+
// `content`/`text`/`response` string must not clobber the answer and
|
|
624
|
+
// look fresh (it would be returned as the final answer).
|
|
625
|
+
const isAnswerUrl = /chat\/(completion|continue)/i.test(url);
|
|
603
626
|
const extracted = extractAnswer(body);
|
|
604
|
-
if (extracted) {
|
|
627
|
+
if (extracted && isAnswerUrl) {
|
|
605
628
|
this._netCapture = extracted;
|
|
606
629
|
this._netNoAnswer = false;
|
|
607
630
|
const cid = url.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
|
|
@@ -612,7 +635,15 @@ export class DeepSeekBrowser {
|
|
|
612
635
|
// Store the RAW body for the CURRENT answer (completion/continue) so the
|
|
613
636
|
// truncated/no-answer detectors can inspect it even when there is no
|
|
614
637
|
// RESPONSE text (extractAnswer() returns '' for those bodies).
|
|
615
|
-
if (
|
|
638
|
+
if (isAnswerUrl) {
|
|
639
|
+
// A completion/continue body that arrives while WE have no ask in
|
|
640
|
+
// flight was the OPERATOR's own turn (they sent a message by hand or
|
|
641
|
+
// resumed in the web UI). Record that moment so our next agent send
|
|
642
|
+
// waits out the throttle from it instead of firing too soon after the
|
|
643
|
+
// manual turn (which caused a rate limit). Do NOT touch the interval.
|
|
644
|
+
if (this._askInFlight === 0) {
|
|
645
|
+
this._lastSentAt = Date.now();
|
|
646
|
+
}
|
|
616
647
|
this._netBody = body;
|
|
617
648
|
const cid = url.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
|
|
618
649
|
if (cid)
|
|
@@ -632,6 +663,15 @@ export class DeepSeekBrowser {
|
|
|
632
663
|
if (isGenerationIncompleteText(body)) {
|
|
633
664
|
this._netCapture = '';
|
|
634
665
|
}
|
|
666
|
+
// The rate limit can arrive ONLY in the SSE body (the DOM toast is
|
|
667
|
+
// short-lived and easy to miss). Recognise it here so ask() waits
|
|
668
|
+
// instead of waiting out the deadline and throwing
|
|
669
|
+
// ds.send_no_new_answer while the chat shows "Messages too
|
|
670
|
+
// frequent".
|
|
671
|
+
if (isRateLimitText(body)) {
|
|
672
|
+
this._netRateLimited = true;
|
|
673
|
+
this._netCapture = '';
|
|
674
|
+
}
|
|
635
675
|
this._netCaptureAt = Date.now();
|
|
636
676
|
}
|
|
637
677
|
}
|
|
@@ -985,18 +1025,61 @@ export class DeepSeekBrowser {
|
|
|
985
1025
|
return false;
|
|
986
1026
|
}
|
|
987
1027
|
}
|
|
1028
|
+
// Wait for the INPUT to be present and visible, polling instead of a
|
|
1029
|
+
// fixed sleep. Returns true as soon as it appears (or false on timeout).
|
|
1030
|
+
async _awaitInput(timeoutMs = 5000) {
|
|
1031
|
+
const deadline = Date.now() + timeoutMs;
|
|
1032
|
+
while (Date.now() < deadline) {
|
|
1033
|
+
const found = await this._findVisible(INPUT_SELECTORS, 400);
|
|
1034
|
+
if (found)
|
|
1035
|
+
return true;
|
|
1036
|
+
await this.page.waitForTimeout(100);
|
|
1037
|
+
}
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
// Did the chat actually CHANGE? Compares the current chat id (URL or the
|
|
1041
|
+
// one sniffed off the network) with a baseline. Used to VERIFY newChat /
|
|
1042
|
+
// openChat instead of blindly trusting a click that may have done nothing.
|
|
1043
|
+
async _chatIdChangedFrom(baseline) {
|
|
1044
|
+
const now = await this.getCurrentChatId();
|
|
1045
|
+
return !!now && now !== baseline;
|
|
1046
|
+
}
|
|
1047
|
+
// Open a fresh chat. Returns as soon as the new-chat input is ready
|
|
1048
|
+
// (polling), and VERIFIES the chat actually changed. A failed click falls
|
|
1049
|
+
// back to navigating to the base URL, and we still wait for a positive
|
|
1050
|
+
// signal before returning, so the next prompt is never sent into the OLD
|
|
1051
|
+
// chat silently.
|
|
988
1052
|
async newChat() {
|
|
1053
|
+
const before = await this.getCurrentChatId();
|
|
989
1054
|
const newChatBtn = this.page
|
|
990
1055
|
.locator('button, a')
|
|
991
1056
|
.filter({ hasText: /new chat|новый чат|новый диалог/i })
|
|
992
1057
|
.first();
|
|
1058
|
+
let clicked = false;
|
|
993
1059
|
try {
|
|
994
1060
|
await newChatBtn.click({ timeout: 2000 });
|
|
995
|
-
|
|
1061
|
+
clicked = true;
|
|
996
1062
|
}
|
|
997
1063
|
catch {
|
|
1064
|
+
// fall through to the goto fallback below
|
|
1065
|
+
}
|
|
1066
|
+
// Give the app a moment to react to the click, then poll for the positive
|
|
1067
|
+
// signal (input ready AND chat changed). If the click did nothing, fall
|
|
1068
|
+
// back to navigating to the base URL.
|
|
1069
|
+
let ok = false;
|
|
1070
|
+
const deadline = Date.now() + 5000;
|
|
1071
|
+
while (Date.now() < deadline) {
|
|
1072
|
+
if (await this._awaitInput(500)) {
|
|
1073
|
+
if (!clicked || (await this._chatIdChangedFrom(before))) {
|
|
1074
|
+
ok = true;
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
await this.page.waitForTimeout(100);
|
|
1079
|
+
}
|
|
1080
|
+
if (!ok) {
|
|
998
1081
|
await this.page.goto(CHAT_URL, { waitUntil: 'domcontentloaded' });
|
|
999
|
-
await this.
|
|
1082
|
+
await this._awaitInput(5000);
|
|
1000
1083
|
}
|
|
1001
1084
|
}
|
|
1002
1085
|
async _findVisible(selectors, timeout = 1000) {
|
|
@@ -1068,9 +1151,12 @@ export class DeepSeekBrowser {
|
|
|
1068
1151
|
return out.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim();
|
|
1069
1152
|
}, ANSWER_SELECTORS);
|
|
1070
1153
|
}
|
|
1071
|
-
// We read ONLY visible toasts/notifications
|
|
1072
|
-
//
|
|
1073
|
-
//
|
|
1154
|
+
// We read ONLY visible toasts/notifications, not the whole page text.
|
|
1155
|
+
// Otherwise we catch "try again later" from service/hidden blocks and go
|
|
1156
|
+
// into a false 5-minute rate-limit wait. The old list also matched a broad
|
|
1157
|
+
// `[class*="error" i]` / `[class*="alert" i]`, which picked up leftover
|
|
1158
|
+
// text from the page and produced a FALSE rate limit (the agent stopped
|
|
1159
|
+
// while the chat had no error). Keep only real toast/alert containers.
|
|
1074
1160
|
async _readPageText() {
|
|
1075
1161
|
return await this.page
|
|
1076
1162
|
.evaluate(() => {
|
|
@@ -1078,8 +1164,6 @@ export class DeepSeekBrowser {
|
|
|
1078
1164
|
'[role="alert"]',
|
|
1079
1165
|
'[class*="toast" i]',
|
|
1080
1166
|
'[class*="notification" i]',
|
|
1081
|
-
'[class*="alert" i]',
|
|
1082
|
-
'[class*="error" i]',
|
|
1083
1167
|
];
|
|
1084
1168
|
let out = '';
|
|
1085
1169
|
for (const s of sels) {
|
|
@@ -1095,13 +1179,9 @@ export class DeepSeekBrowser {
|
|
|
1095
1179
|
})
|
|
1096
1180
|
.catch(() => '');
|
|
1097
1181
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
}
|
|
1102
|
-
// The same "clean" filter as _readLastAnswerTextClean, but read from the
|
|
1103
|
-
// DOM only. Used by _askOnce for the before/after comparison: the network
|
|
1104
|
-
// capture must not be compared against itself (see _readLastAnswerTextDom).
|
|
1182
|
+
// The "clean" filter, but read from the DOM only. Used by _askOnce for the
|
|
1183
|
+
// before/after comparison: the network capture must not be compared against
|
|
1184
|
+
// itself (see _readLastAnswerTextDom).
|
|
1105
1185
|
async _readLastAnswerTextCleanDom() {
|
|
1106
1186
|
const raw = await this._readLastAnswerTextDom().catch(() => '');
|
|
1107
1187
|
return this._cleanAnswer(raw);
|
|
@@ -1150,11 +1230,65 @@ export class DeepSeekBrowser {
|
|
|
1150
1230
|
async _isGenerating() {
|
|
1151
1231
|
return await this._stopButtonVisible();
|
|
1152
1232
|
}
|
|
1233
|
+
// A CHEAP growth signal for the "did the answer start?" loop. The old
|
|
1234
|
+
// code read `document.body.innerText.length` every ~300ms tick, which
|
|
1235
|
+
// serializes the ENTIRE page text (sidebar, history, menus) and forces a
|
|
1236
|
+
// layout — expensive on a long chat. This returns only the number of
|
|
1237
|
+
// message/answer nodes plus the length of the LAST answer element, which is
|
|
1238
|
+
// all we need to tell "the page grew / a new answer is streaming".
|
|
1239
|
+
async _chatSignal() {
|
|
1240
|
+
return await this.page
|
|
1241
|
+
.evaluate(() => {
|
|
1242
|
+
const nodes = document.querySelectorAll('[class*="ds-message"], [class*="chat-message"], .ds-markdown').length;
|
|
1243
|
+
let lastLen = 0;
|
|
1244
|
+
const answers = document.querySelectorAll('.ds-markdown');
|
|
1245
|
+
if (answers.length) {
|
|
1246
|
+
const lastEl = answers[answers.length - 1];
|
|
1247
|
+
lastLen = (lastEl.innerText || '').length;
|
|
1248
|
+
}
|
|
1249
|
+
return { nodes, lastLen };
|
|
1250
|
+
})
|
|
1251
|
+
.catch(() => ({ nodes: 0, lastLen: 0 }));
|
|
1252
|
+
}
|
|
1253
|
+
// Cheap first probe for the Continue button. Runs ONE page.evaluate
|
|
1254
|
+
// that scans buttons/role-buttons for a matching label and returns a boolean.
|
|
1255
|
+
// The full Playwright getByRole scan (count + isVisible per element) plus the
|
|
1256
|
+
// DOM fallback are two round-trips; on the COMMON path (no button on screen,
|
|
1257
|
+
// probed every ~300ms tick) this single evaluate replaces them. Only when it
|
|
1258
|
+
// HITS do we run the full scan to get a clickable Locator.
|
|
1259
|
+
async _continueVisibleCheap() {
|
|
1260
|
+
return await this.page
|
|
1261
|
+
.evaluate(() => {
|
|
1262
|
+
const re = /^(?:continue|продолжить|продолжение)(?:\s+(?:think(?:ing)?|reason(?:ing)?|размышлени[ея]|генераци[юя]|ответ))?\s*[.!…]?$/i;
|
|
1263
|
+
const cands = Array.from(document.querySelectorAll('div[role="button"], button'));
|
|
1264
|
+
for (const e of cands) {
|
|
1265
|
+
const label = ((e.textContent || '') +
|
|
1266
|
+
' ' +
|
|
1267
|
+
(e.getAttribute('aria-label') || ''))
|
|
1268
|
+
.replace(/\s+/g, ' ')
|
|
1269
|
+
.trim();
|
|
1270
|
+
if (!re.test(label))
|
|
1271
|
+
continue;
|
|
1272
|
+
const st = getComputedStyle(e);
|
|
1273
|
+
if (st.display === 'none' || st.visibility === 'hidden')
|
|
1274
|
+
continue;
|
|
1275
|
+
const r = e.getBoundingClientRect();
|
|
1276
|
+
if (r.width > 0 && r.height > 0)
|
|
1277
|
+
return true;
|
|
1278
|
+
}
|
|
1279
|
+
return false;
|
|
1280
|
+
})
|
|
1281
|
+
.catch(() => false);
|
|
1282
|
+
}
|
|
1153
1283
|
// Is DeepSeek's "Continue" button currently visible? Used to BLOCK the
|
|
1154
1284
|
// "answer settled" early-return in the finish loop: while Continue is on
|
|
1155
1285
|
// screen the generation is PAUSED, not finished, so returning the partial
|
|
1156
1286
|
// text there is exactly the "agent stopped with a Continue button" bug.
|
|
1157
1287
|
async _continueButtonVisible() {
|
|
1288
|
+
// Cheap first probe: on the common path (no button) this single evaluate
|
|
1289
|
+
// replaces the getByRole count+isVisible scan and the DOM fallback.
|
|
1290
|
+
if (!(await this._continueVisibleCheap()))
|
|
1291
|
+
return false;
|
|
1158
1292
|
try {
|
|
1159
1293
|
const loc = this.page.getByRole('button', { name: CONTINUE_NAME_RE });
|
|
1160
1294
|
const n = await loc.count().catch(() => 0);
|
|
@@ -1199,6 +1333,11 @@ export class DeepSeekBrowser {
|
|
|
1199
1333
|
// only act on an EXACT short label so a random "Continue" in prose (a button
|
|
1200
1334
|
// inside a rendered answer, etc.) is never clicked.
|
|
1201
1335
|
async _clickContinueIfVisible() {
|
|
1336
|
+
// Cheap first probe. The common case is "no button" and this is
|
|
1337
|
+
// called every tick from the start-loop; the single evaluate replaces the
|
|
1338
|
+
// getByRole count+isVisible scan plus the DOM fallback on that path.
|
|
1339
|
+
if (!(await this._continueVisibleCheap()))
|
|
1340
|
+
return false;
|
|
1202
1341
|
// 1) Playwright's OWN click on the accessible name — this is a TRUSTED
|
|
1203
1342
|
// event and reliably triggers React handlers. The old in-page
|
|
1204
1343
|
// dispatchEvent/click often did nothing, which is exactly why the
|
|
@@ -1252,6 +1391,10 @@ export class DeepSeekBrowser {
|
|
|
1252
1391
|
try {
|
|
1253
1392
|
await found.click({ timeout: 2000 });
|
|
1254
1393
|
this._lastContinueAt = Date.now();
|
|
1394
|
+
// The auto-click is otherwise invisible. Emit ONE notice per
|
|
1395
|
+
// click (the continueMinGapMs above already limits how often this
|
|
1396
|
+
// can fire) so the operator sees why the turn resumed.
|
|
1397
|
+
this._notice(this._t('ds.continue_clicked'));
|
|
1255
1398
|
return true;
|
|
1256
1399
|
}
|
|
1257
1400
|
catch { }
|
|
@@ -1297,6 +1440,10 @@ export class DeepSeekBrowser {
|
|
|
1297
1440
|
}
|
|
1298
1441
|
return false;
|
|
1299
1442
|
});
|
|
1443
|
+
if (clicked) {
|
|
1444
|
+
this._lastContinueAt = Date.now();
|
|
1445
|
+
this._notice(this._t('ds.continue_clicked'));
|
|
1446
|
+
}
|
|
1300
1447
|
return !!clicked;
|
|
1301
1448
|
}
|
|
1302
1449
|
catch {
|
|
@@ -1370,9 +1517,29 @@ export class DeepSeekBrowser {
|
|
|
1370
1517
|
webSearch: await read(/поиск|search/i, this.webSearch),
|
|
1371
1518
|
};
|
|
1372
1519
|
}
|
|
1520
|
+
// Stop the current generation. Used by Esc/Ctrl+C. Prefers a TRUSTED
|
|
1521
|
+
// Playwright click on the Stop button (matched by its accessible name, the
|
|
1522
|
+
// same technique as _clickContinueIfVisible): an in-page `e.click()` /
|
|
1523
|
+
// dispatchEvent is IGNORED by DeepSeek's React button, so the old DOM-only
|
|
1524
|
+
// path often did not stop anything. Falls back to a raw DOM scan and finally
|
|
1525
|
+
// to Escape.
|
|
1373
1526
|
async stopGeneration() {
|
|
1374
1527
|
this._abort = true;
|
|
1375
1528
|
this._stopped = true;
|
|
1529
|
+
// 1) Trusted click on the accessible name.
|
|
1530
|
+
try {
|
|
1531
|
+
const loc = this.page.getByRole('button', { name: STOP_NAME_RE });
|
|
1532
|
+
const n = await loc.count().catch(() => 0);
|
|
1533
|
+
for (let i = 0; i < n; i++) {
|
|
1534
|
+
const b = loc.nth(i);
|
|
1535
|
+
if (await b.isVisible().catch(() => false)) {
|
|
1536
|
+
await b.click({ timeout: 2000 });
|
|
1537
|
+
return true;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
catch { }
|
|
1542
|
+
// 2) Legacy selector path.
|
|
1376
1543
|
const btn = await this._findVisible(STOP_SELECTORS, 500);
|
|
1377
1544
|
if (btn) {
|
|
1378
1545
|
try {
|
|
@@ -1381,6 +1548,8 @@ export class DeepSeekBrowser {
|
|
|
1381
1548
|
}
|
|
1382
1549
|
catch { }
|
|
1383
1550
|
}
|
|
1551
|
+
// 3) Raw DOM scan + the full pointer/mouse sequence (older builds without
|
|
1552
|
+
// a proper role, or a click intercepted by an overlay).
|
|
1384
1553
|
const clicked = await this.page
|
|
1385
1554
|
.evaluate(() => {
|
|
1386
1555
|
const btns = Array.from(document.querySelectorAll('div[role="button"], button'));
|
|
@@ -1390,6 +1559,15 @@ export class DeepSeekBrowser {
|
|
|
1390
1559
|
continue;
|
|
1391
1560
|
const svg = b.querySelector('svg');
|
|
1392
1561
|
if (svg && svg.querySelector('rect')) {
|
|
1562
|
+
for (const type of [
|
|
1563
|
+
'pointerdown',
|
|
1564
|
+
'mousedown',
|
|
1565
|
+
'pointerup',
|
|
1566
|
+
'mouseup',
|
|
1567
|
+
'click',
|
|
1568
|
+
]) {
|
|
1569
|
+
b.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true }));
|
|
1570
|
+
}
|
|
1393
1571
|
b.click();
|
|
1394
1572
|
return true;
|
|
1395
1573
|
}
|
|
@@ -1416,116 +1594,139 @@ export class DeepSeekBrowser {
|
|
|
1416
1594
|
// hiccup, not a failed send, and it is more likely in the reasoning
|
|
1417
1595
|
// (deep thinking) mode, where the long THINK phase gets cut off.
|
|
1418
1596
|
let incompleteRetries = 0;
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1597
|
+
// Mark that OUR code has an ask in flight. A completion/continue body
|
|
1598
|
+
// that arrives with _askInFlight === 0 was the operator's own turn, and
|
|
1599
|
+
// _onResponse uses that to bump _lastSentAt so the next agent send waits
|
|
1600
|
+
// out the throttle.
|
|
1601
|
+
this._askInFlight++;
|
|
1602
|
+
try {
|
|
1603
|
+
// NOTE: `attempt` counts only GENUINE failures. The three retryable
|
|
1604
|
+
// classes below (rate limit / server busy / truncated turn) have their OWN
|
|
1605
|
+
// budgets and must NOT consume an ask() attempt — otherwise a rate limit
|
|
1606
|
+
// at the start burns the whole askRetries budget and ask() gives up early.
|
|
1607
|
+
while (attempt < this.askRetries) {
|
|
1608
|
+
try {
|
|
1609
|
+
return await this._askOnce(prompt, { timeout, agent, attachments });
|
|
1610
|
+
}
|
|
1611
|
+
catch (e) {
|
|
1612
|
+
lastErr = e;
|
|
1613
|
+
// Rate limit: DeepSeek did not accept the message. We wait a long time
|
|
1614
|
+
// and resend into the SAME chat (without newChat — otherwise the
|
|
1615
|
+
// context is lost). The waits don't consume the regular ask() attempts.
|
|
1616
|
+
if (e instanceof RateLimitError) {
|
|
1617
|
+
rateLimitRetries++;
|
|
1618
|
+
if (rateLimitRetries > this.maxRateLimitRetries) {
|
|
1619
|
+
this._notice(theme.error(this._t('ds.rate_limit_give_up', {
|
|
1620
|
+
attempt: rateLimitRetries,
|
|
1621
|
+
min: Math.ceil(this.rateLimitWaitMs / 60000),
|
|
1622
|
+
})));
|
|
1623
|
+
throw e;
|
|
1624
|
+
}
|
|
1625
|
+
this._notice(theme.warn(this._t('ds.rate_limit_wait', {
|
|
1434
1626
|
min: Math.ceil(this.rateLimitWaitMs / 60000),
|
|
1627
|
+
attempt: rateLimitRetries,
|
|
1628
|
+
max: this.maxRateLimitRetries,
|
|
1435
1629
|
})));
|
|
1436
|
-
|
|
1630
|
+
// Interruptible: Esc/Ctrl+C must cancel this long wait too,
|
|
1631
|
+
// otherwise "stop" stays frozen for up to 5 minutes.
|
|
1632
|
+
// The wait is ANIMATED through onSendPause (a live countdown), so the
|
|
1633
|
+
// operator sees the agent is waiting on the rate limit instead of a
|
|
1634
|
+
// frozen spinner. `_lastSentAt` is bumped so the countdown renders.
|
|
1635
|
+
const waitSec = Math.ceil(this.rateLimitWaitMs / 1000);
|
|
1636
|
+
const aborted = await this._waitRateLimit(waitSec);
|
|
1637
|
+
if (aborted)
|
|
1638
|
+
return '(прервано пользователем)';
|
|
1639
|
+
continue;
|
|
1437
1640
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
continue;
|
|
1453
|
-
}
|
|
1454
|
-
// Server busy / overloaded: clear in seconds, so we retry quickly
|
|
1455
|
-
// (unlike the rate limit). Without this branch the error fell into the
|
|
1456
|
-
// generic ask() retry and the operator saw a bare message with no
|
|
1457
|
-
// explanation of what DeepSeek is doing.
|
|
1458
|
-
if (e instanceof ServerBusyError) {
|
|
1459
|
-
serverBusyRetries++;
|
|
1460
|
-
if (serverBusyRetries > this.maxServerBusyRetries) {
|
|
1461
|
-
this._notice(theme.error(this._t('ds.server_busy_give_up', {
|
|
1641
|
+
// Server busy / overloaded: clear in seconds, so we retry quickly
|
|
1642
|
+
// (unlike the rate limit). Without this branch the error fell into the
|
|
1643
|
+
// generic ask() retry and the operator saw a bare message with no
|
|
1644
|
+
// explanation of what DeepSeek is doing.
|
|
1645
|
+
if (e instanceof ServerBusyError) {
|
|
1646
|
+
serverBusyRetries++;
|
|
1647
|
+
if (serverBusyRetries > this.maxServerBusyRetries) {
|
|
1648
|
+
this._notice(theme.error(this._t('ds.server_busy_give_up', {
|
|
1649
|
+
attempt: serverBusyRetries,
|
|
1650
|
+
})));
|
|
1651
|
+
throw e;
|
|
1652
|
+
}
|
|
1653
|
+
this._notice(theme.warn(this._t('ds.server_busy_wait', {
|
|
1654
|
+
sec: Math.ceil(this.serverBusyWaitMs / 1000),
|
|
1462
1655
|
attempt: serverBusyRetries,
|
|
1656
|
+
max: this.maxServerBusyRetries,
|
|
1463
1657
|
})));
|
|
1464
|
-
|
|
1658
|
+
const aborted = await this._sleepInterruptible(this.serverBusyWaitMs);
|
|
1659
|
+
if (aborted)
|
|
1660
|
+
return '(прервано пользователем)';
|
|
1661
|
+
continue;
|
|
1465
1662
|
}
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
// button does) instead of waiting for an answer that will never come.
|
|
1481
|
-
if (e instanceof GenerationIncompleteError) {
|
|
1482
|
-
incompleteRetries++;
|
|
1483
|
-
if (incompleteRetries > this.maxIncompleteRetries) {
|
|
1484
|
-
this._notice(theme.error(this._t('ds.incomplete_give_up', {
|
|
1663
|
+
// The turn was TRUNCATED by the server (`generation_err`, INCOMPLETE):
|
|
1664
|
+
// DeepSeek answered with a partial (often empty) answer and shows a
|
|
1665
|
+
// "Continue" button. In reasoning mode this is much more likely. We
|
|
1666
|
+
// resend the SAME prompt into the chat (that is what the Continue
|
|
1667
|
+
// button does) instead of waiting for an answer that will never come.
|
|
1668
|
+
if (e instanceof GenerationIncompleteError) {
|
|
1669
|
+
incompleteRetries++;
|
|
1670
|
+
if (incompleteRetries > this.maxIncompleteRetries) {
|
|
1671
|
+
this._notice(theme.error(this._t('ds.incomplete_give_up', {
|
|
1672
|
+
attempt: incompleteRetries,
|
|
1673
|
+
})));
|
|
1674
|
+
throw e;
|
|
1675
|
+
}
|
|
1676
|
+
this._notice(theme.warn(this._t('ds.incomplete_retry', {
|
|
1485
1677
|
attempt: incompleteRetries,
|
|
1678
|
+
max: this.maxIncompleteRetries,
|
|
1486
1679
|
})));
|
|
1487
|
-
|
|
1680
|
+
const aborted = await this._sleepInterruptible(this.incompleteWaitMs);
|
|
1681
|
+
if (aborted)
|
|
1682
|
+
return '(прервано пользователем)';
|
|
1683
|
+
continue;
|
|
1488
1684
|
}
|
|
1489
|
-
this._notice(theme.warn(this._t('ds.
|
|
1490
|
-
attempt:
|
|
1491
|
-
max: this.
|
|
1685
|
+
this._notice(theme.warn(this._t('ds.ask_retry', {
|
|
1686
|
+
attempt: attempt + 1,
|
|
1687
|
+
max: this.askRetries,
|
|
1688
|
+
error: e.message,
|
|
1492
1689
|
})));
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
this._notice(theme.warn(this._t('ds.ask_restart_browser')));
|
|
1510
|
-
try {
|
|
1511
|
-
await this.restart();
|
|
1512
|
-
await this.waitForLogin();
|
|
1690
|
+
// Only a genuinely LOST page justifies a restart. "Target page,
|
|
1691
|
+
// context or browser has been closed" means the page is gone for good;
|
|
1692
|
+
// a bare "browser" mention (e.g. a selector error containing the word)
|
|
1693
|
+
// must NOT tear the window down — that was the "a new window opened"
|
|
1694
|
+
// surprise during a resumed chat. We re-open the SAME context instead.
|
|
1695
|
+
if (/target page, context or browser has been closed|page.*has been closed/i.test(e.message)) {
|
|
1696
|
+
this._notice(theme.warn(this._t('ds.ask_restart_browser')));
|
|
1697
|
+
try {
|
|
1698
|
+
await this.restart();
|
|
1699
|
+
await this.waitForLogin();
|
|
1700
|
+
}
|
|
1701
|
+
catch (re) {
|
|
1702
|
+
this._notice(theme.warn(this._t('ds.ask_restart_failed', {
|
|
1703
|
+
error: re.message,
|
|
1704
|
+
})));
|
|
1705
|
+
}
|
|
1513
1706
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
error: re.message,
|
|
1517
|
-
})));
|
|
1707
|
+
if (attempt < this.askRetries - 1) {
|
|
1708
|
+
await new Promise((r) => setTimeout(r, 2000 * (attempt + 1)));
|
|
1518
1709
|
}
|
|
1519
|
-
|
|
1520
|
-
if (attempt < this.askRetries) {
|
|
1521
|
-
await new Promise((r) => setTimeout(r, 2000 * attempt));
|
|
1710
|
+
attempt++;
|
|
1522
1711
|
}
|
|
1523
1712
|
}
|
|
1713
|
+
throw new Error(this._t('ds.ask_failed', {
|
|
1714
|
+
max: this.askRetries,
|
|
1715
|
+
error: lastErr?.message || '',
|
|
1716
|
+
}));
|
|
1524
1717
|
}
|
|
1525
|
-
|
|
1526
|
-
max
|
|
1527
|
-
|
|
1528
|
-
|
|
1718
|
+
finally {
|
|
1719
|
+
this._askInFlight = Math.max(0, this._askInFlight - 1);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
// Cancel an in-flight ask() whose caller (the agent-loop watchdog) already
|
|
1723
|
+
// gave up on it. WITHOUT this the abandoned ask() kept running — it could
|
|
1724
|
+
// still be inside `_waitRateLimit` (up to 300s) or the finish loop — while
|
|
1725
|
+
// the NEXT iteration started a second ask() against the same page. Two asks
|
|
1726
|
+
// then both send / both click Continue: a duplicate message and possible
|
|
1727
|
+
// rate-limit hammering. The flag is reset at the start of every _askOnce.
|
|
1728
|
+
cancelPendingAsk() {
|
|
1729
|
+
this._askCancelled = true;
|
|
1529
1730
|
}
|
|
1530
1731
|
async _setInputText(input, text) {
|
|
1531
1732
|
const tag = await input.evaluate((el) => el.tagName.toLowerCase());
|
|
@@ -1631,7 +1832,8 @@ export class DeepSeekBrowser {
|
|
|
1631
1832
|
btn.click({ timeout: 1500 }),
|
|
1632
1833
|
]);
|
|
1633
1834
|
await chooser.setFiles(bufPayload);
|
|
1634
|
-
|
|
1835
|
+
// Wait for the upload preview instead of a fixed 2.5s sleep.
|
|
1836
|
+
await this._awaitUploadPreview(files.map((f) => f.name), 2500);
|
|
1635
1837
|
return;
|
|
1636
1838
|
}
|
|
1637
1839
|
catch { }
|
|
@@ -1650,7 +1852,32 @@ export class DeepSeekBrowser {
|
|
|
1650
1852
|
return;
|
|
1651
1853
|
}
|
|
1652
1854
|
// Wait for the upload to finish (the attach preview to appear).
|
|
1653
|
-
await this.
|
|
1855
|
+
await this._awaitUploadPreview(files.map((f) => f.name), 2000);
|
|
1856
|
+
}
|
|
1857
|
+
// Wait for the upload preview (the attached-file chip above the
|
|
1858
|
+
// input) to appear, instead of a fixed 2.5s/2s sleep. The signal is the
|
|
1859
|
+
// FILE NAME showing up on the page: we attach files BEFORE typing the text,
|
|
1860
|
+
// so the name cannot appear there for any other reason. The old fixed value
|
|
1861
|
+
// stays as the TIMEOUT CEILING, so a build where the name is not detected
|
|
1862
|
+
// behaves exactly as before.
|
|
1863
|
+
async _awaitUploadPreview(names, timeoutMs) {
|
|
1864
|
+
const clean = names.map((n) => String(n || '').trim()).filter(Boolean);
|
|
1865
|
+
if (!clean.length) {
|
|
1866
|
+
await this.page.waitForTimeout(timeoutMs);
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
const deadline = Date.now() + timeoutMs;
|
|
1870
|
+
while (Date.now() < deadline) {
|
|
1871
|
+
for (const name of clean) {
|
|
1872
|
+
const n = await this.page
|
|
1873
|
+
.getByText(name, { exact: false })
|
|
1874
|
+
.count()
|
|
1875
|
+
.catch(() => 0);
|
|
1876
|
+
if (n > 0)
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
await this.page.waitForTimeout(150);
|
|
1880
|
+
}
|
|
1654
1881
|
}
|
|
1655
1882
|
// Wait out a rate-limit pause, showing an animated countdown through
|
|
1656
1883
|
// onSendPause so the operator sees the agent is alive (not frozen). Returns
|
|
@@ -1724,6 +1951,10 @@ export class DeepSeekBrowser {
|
|
|
1724
1951
|
while (left > 0) {
|
|
1725
1952
|
if (this._abort)
|
|
1726
1953
|
return true;
|
|
1954
|
+
// The watchdog abandoned this ask(): stop sleeping so the next ask()
|
|
1955
|
+
// can start without two asks competing for the same page.
|
|
1956
|
+
if (this._askCancelled)
|
|
1957
|
+
return true;
|
|
1727
1958
|
const chunk = Math.min(step, left);
|
|
1728
1959
|
await this.page.waitForTimeout(chunk);
|
|
1729
1960
|
left -= chunk;
|
|
@@ -1755,6 +1986,8 @@ export class DeepSeekBrowser {
|
|
|
1755
1986
|
// set while a previous send was waiting in the pause. If it is set, the
|
|
1756
1987
|
// user asked to stop and we must not start a new generation at all.
|
|
1757
1988
|
this._abort = false;
|
|
1989
|
+
// A fresh send clears a stale cancellation from a previous, abandoned ask.
|
|
1990
|
+
this._askCancelled = false;
|
|
1758
1991
|
if (this._stopped)
|
|
1759
1992
|
return '(прервано пользователем)';
|
|
1760
1993
|
const input = await this._findVisible(INPUT_SELECTORS, 10_000);
|
|
@@ -1792,6 +2025,7 @@ export class DeepSeekBrowser {
|
|
|
1792
2025
|
this._netCapture = '';
|
|
1793
2026
|
this._netCaptureAt = 0;
|
|
1794
2027
|
this._netNoAnswer = false;
|
|
2028
|
+
this._netRateLimited = false;
|
|
1795
2029
|
this._netBody = '';
|
|
1796
2030
|
// Align the DeepSeek chat toggles (deep thinking / web search) with the
|
|
1797
2031
|
// configured state BEFORE typing. Doing it here (after the send-pause) it
|
|
@@ -1803,7 +2037,9 @@ export class DeepSeekBrowser {
|
|
|
1803
2037
|
await this._attachFiles(attachments);
|
|
1804
2038
|
}
|
|
1805
2039
|
await this._setInputText(input, prompt);
|
|
1806
|
-
|
|
2040
|
+
// Send immediately: the input is already verified by _setInputText, and a
|
|
2041
|
+
// clickable send button normally appears right away. The old code slept a
|
|
2042
|
+
// fixed 50ms here, which only added latency (the button does not need it).
|
|
1807
2043
|
let sent = false;
|
|
1808
2044
|
for (const sel of SEND_SELECTORS) {
|
|
1809
2045
|
const btn = this.page.locator(sel).last();
|
|
@@ -1824,12 +2060,12 @@ export class DeepSeekBrowser {
|
|
|
1824
2060
|
this._lastSentAt = Date.now();
|
|
1825
2061
|
this._askDebug('SENT at=' + this._lastSentAt);
|
|
1826
2062
|
// Wait for the start: either Stop appeared, or the answer text changed,
|
|
1827
|
-
// or the
|
|
1828
|
-
// the rate-limit toast (only toasts, not the whole body).
|
|
2063
|
+
// or the page grew (a new answer node / longer last answer). In parallel we
|
|
2064
|
+
// catch the rate-limit toast (only toasts, not the whole body).
|
|
1829
2065
|
const startDeadline = Date.now() + 30_000;
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
2066
|
+
// A CHEAP growth signal (message-node count + last-answer length)
|
|
2067
|
+
// instead of document.body.innerText.length, which walked the whole page.
|
|
2068
|
+
const startSignal = await this._chatSignal();
|
|
1833
2069
|
let started = false;
|
|
1834
2070
|
// A stale/echo answer (the model repeats the previous text, or the answer
|
|
1835
2071
|
// legitimately equals it) does NOT change `cur`. In that case the old loop
|
|
@@ -1840,7 +2076,7 @@ export class DeepSeekBrowser {
|
|
|
1840
2076
|
let settledTicks = 0;
|
|
1841
2077
|
let lastStartCur = '';
|
|
1842
2078
|
while (Date.now() < startDeadline) {
|
|
1843
|
-
if (this._abort)
|
|
2079
|
+
if (this._abort || this._askCancelled)
|
|
1844
2080
|
return '(прервано пользователем)';
|
|
1845
2081
|
const pageText = await this._readPageText();
|
|
1846
2082
|
if (isRateLimitText(pageText)) {
|
|
@@ -1849,6 +2085,12 @@ export class DeepSeekBrowser {
|
|
|
1849
2085
|
if (isServerBusyText(pageText)) {
|
|
1850
2086
|
throw new ServerBusyError(pageText.slice(0, 300));
|
|
1851
2087
|
}
|
|
2088
|
+
// The rate limit may be visible ONLY in the SSE body (the DOM toast is
|
|
2089
|
+
// short-lived and easy to miss). The old code threw ds.send_no_new_answer
|
|
2090
|
+
// at the deadline while the chat plainly showed "Messages too frequent".
|
|
2091
|
+
if (this._netRateLimited && this._netCaptureAt >= this._lastSentAt) {
|
|
2092
|
+
throw new RateLimitError(this._netBody.slice(0, 300) || 'rate limit (from network)');
|
|
2093
|
+
}
|
|
1852
2094
|
// The turn was truncated by the server: the SSE body carries
|
|
1853
2095
|
// finish_reason=generation_err / quasi_status=INCOMPLETE and the UI
|
|
1854
2096
|
// shows "Continue". Retry instead of waiting out the whole timeout.
|
|
@@ -1891,9 +2133,9 @@ export class DeepSeekBrowser {
|
|
|
1891
2133
|
this._askDebug('CLICKED Continue (start-loop)');
|
|
1892
2134
|
}
|
|
1893
2135
|
const cur = await this._readLastAnswerTextCleanDom().catch(() => '');
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
2136
|
+
// Cheap growth signal instead of the whole-body length.
|
|
2137
|
+
const sig = await this._chatSignal();
|
|
2138
|
+
const grew = sig.nodes > startSignal.nodes || sig.lastLen > startSignal.lastLen;
|
|
1897
2139
|
// A NEW answer is the only reliable sign that the message was actually
|
|
1898
2140
|
// sent: the text on the page must differ from what was there before the
|
|
1899
2141
|
// send, OR the page must have grown. The Stop-button heuristic
|
|
@@ -1908,14 +2150,14 @@ export class DeepSeekBrowser {
|
|
|
1908
2150
|
// 'changed' can stay false for a while — without this check ask() used
|
|
1909
2151
|
// to hang until the full timeout and the agent appeared to "stop".
|
|
1910
2152
|
const netStarted = !!this._netCapture && this._netCaptureAt >= this._lastSentAt;
|
|
1911
|
-
if (changed || netStarted ||
|
|
2153
|
+
if (changed || netStarted || grew) {
|
|
1912
2154
|
started = true;
|
|
1913
2155
|
this._askDebug('STARTED changed=' +
|
|
1914
2156
|
changed +
|
|
1915
2157
|
' netStarted=' +
|
|
1916
2158
|
netStarted +
|
|
1917
|
-
'
|
|
1918
|
-
|
|
2159
|
+
' grew=' +
|
|
2160
|
+
grew);
|
|
1919
2161
|
break;
|
|
1920
2162
|
}
|
|
1921
2163
|
// Fallback for an echo: the text equals beforeText, so it is the OLD
|
|
@@ -1945,8 +2187,8 @@ export class DeepSeekBrowser {
|
|
|
1945
2187
|
changed +
|
|
1946
2188
|
' netStarted=' +
|
|
1947
2189
|
netStarted +
|
|
1948
|
-
'
|
|
1949
|
-
|
|
2190
|
+
' grew=' +
|
|
2191
|
+
grew +
|
|
1950
2192
|
' settled=' +
|
|
1951
2193
|
settledTicks +
|
|
1952
2194
|
' generating=' +
|
|
@@ -1999,13 +2241,12 @@ export class DeepSeekBrowser {
|
|
|
1999
2241
|
this._lastSentAt = Date.now();
|
|
2000
2242
|
const retryDeadline = Date.now() + 20_000;
|
|
2001
2243
|
while (Date.now() < retryDeadline) {
|
|
2002
|
-
if (this._abort)
|
|
2244
|
+
if (this._abort || this._askCancelled)
|
|
2003
2245
|
return '(прервано пользователем)';
|
|
2004
2246
|
const cur2 = await this._readLastAnswerTextCleanDom().catch(() => '');
|
|
2005
2247
|
const net2 = !!this._netCapture && this._netCaptureAt >= this._lastSentAt;
|
|
2006
|
-
const
|
|
2007
|
-
|
|
2008
|
-
.catch(() => 0)) > startBodyLen;
|
|
2248
|
+
const sig2 = await this._chatSignal();
|
|
2249
|
+
const grew2 = sig2.nodes > startSignal.nodes || sig2.lastLen > startSignal.lastLen;
|
|
2009
2250
|
if (net2 ||
|
|
2010
2251
|
grew2 ||
|
|
2011
2252
|
(!!cur2 && normText(cur2) !== normText(beforeText))) {
|
|
@@ -2027,7 +2268,7 @@ export class DeepSeekBrowser {
|
|
|
2027
2268
|
let last = '';
|
|
2028
2269
|
let stable = 0;
|
|
2029
2270
|
while (Date.now() < deadline) {
|
|
2030
|
-
if (this._abort) {
|
|
2271
|
+
if (this._abort || this._askCancelled) {
|
|
2031
2272
|
return last || '(прервано пользователем)';
|
|
2032
2273
|
}
|
|
2033
2274
|
// Rate-limit / server-busy toast. The old code only read the toasts
|
|
@@ -2046,6 +2287,12 @@ export class DeepSeekBrowser {
|
|
|
2046
2287
|
throw new ServerBusyError(pageText.slice(0, 300));
|
|
2047
2288
|
}
|
|
2048
2289
|
}
|
|
2290
|
+
// The rate limit may exist only in the SSE body (the DOM toast is
|
|
2291
|
+
// short-lived). Without this the loop waited out the deadline and threw
|
|
2292
|
+
// ds.send_no_new_answer while the chat showed "Messages too frequent".
|
|
2293
|
+
if (this._netRateLimited && this._netCaptureAt >= this._lastSentAt) {
|
|
2294
|
+
throw new RateLimitError(this._netBody.slice(0, 300) || 'rate limit (from network)');
|
|
2295
|
+
}
|
|
2049
2296
|
// The server truncated the turn (generation_err / INCOMPLETE) or ended
|
|
2050
2297
|
// it with only reasoning. PREFER the Continue button: it RESUMES the
|
|
2051
2298
|
// same turn (`chat/continue`). Resending the prompt creates a NEW turn
|
|
@@ -2120,7 +2367,11 @@ export class DeepSeekBrowser {
|
|
|
2120
2367
|
}
|
|
2121
2368
|
stable = 0;
|
|
2122
2369
|
}
|
|
2123
|
-
else if (
|
|
2370
|
+
else if (!(await this._isGenerating())) {
|
|
2371
|
+
// Only return when the generation has REALLY ended — no Stop
|
|
2372
|
+
// button AND no Continue button (checked above). The old condition
|
|
2373
|
+
// `isNew || !_isGenerating()` returned a still-streaming answer
|
|
2374
|
+
// merely because it looked "new", cutting a long answer short.
|
|
2124
2375
|
this._askDebug('RETURN stable curLen=' + cur.length);
|
|
2125
2376
|
return cur;
|
|
2126
2377
|
}
|
|
@@ -2249,6 +2500,8 @@ export class DeepSeekBrowser {
|
|
|
2249
2500
|
}, limit);
|
|
2250
2501
|
}
|
|
2251
2502
|
async openChat(id) {
|
|
2503
|
+
// Click the chat link and VERIFY the URL actually points at the
|
|
2504
|
+
// requested chat before returning (polling, no fixed 1.5s sleep).
|
|
2252
2505
|
const candidates = [`a[href$="/chat/s/${id}"]`, `a[href*="${id}"]`];
|
|
2253
2506
|
for (const sel of candidates) {
|
|
2254
2507
|
try {
|
|
@@ -2256,8 +2509,14 @@ export class DeepSeekBrowser {
|
|
|
2256
2509
|
if ((await loc.count()) === 0)
|
|
2257
2510
|
continue;
|
|
2258
2511
|
await loc.click({ timeout: 3000 });
|
|
2259
|
-
|
|
2260
|
-
|
|
2512
|
+
const deadline = Date.now() + 8000;
|
|
2513
|
+
while (Date.now() < deadline) {
|
|
2514
|
+
if ((await this.getCurrentChatId()) === id) {
|
|
2515
|
+
await this._awaitInput(2000);
|
|
2516
|
+
return true;
|
|
2517
|
+
}
|
|
2518
|
+
await this.page.waitForTimeout(100);
|
|
2519
|
+
}
|
|
2261
2520
|
}
|
|
2262
2521
|
catch { }
|
|
2263
2522
|
}
|
|
@@ -2266,8 +2525,18 @@ export class DeepSeekBrowser {
|
|
|
2266
2525
|
waitUntil: 'domcontentloaded',
|
|
2267
2526
|
timeout: 20_000,
|
|
2268
2527
|
});
|
|
2269
|
-
|
|
2270
|
-
|
|
2528
|
+
// Verify the navigation really landed on the requested chat; otherwise
|
|
2529
|
+
// report the failure instead of sending the next prompt into the wrong
|
|
2530
|
+
// chat silently.
|
|
2531
|
+
const deadline = Date.now() + 10_000;
|
|
2532
|
+
while (Date.now() < deadline) {
|
|
2533
|
+
if ((await this.getCurrentChatId()) === id) {
|
|
2534
|
+
await this._awaitInput(2000);
|
|
2535
|
+
return true;
|
|
2536
|
+
}
|
|
2537
|
+
await this.page.waitForTimeout(100);
|
|
2538
|
+
}
|
|
2539
|
+
throw new Error('chat id did not change to ' + id);
|
|
2271
2540
|
}
|
|
2272
2541
|
catch (e) {
|
|
2273
2542
|
throw new Error(this._t('ds.open_chat_failed', { id, error: e.message }));
|
|
@@ -2461,6 +2730,14 @@ export class DeepSeekBrowser {
|
|
|
2461
2730
|
break;
|
|
2462
2731
|
}
|
|
2463
2732
|
}
|
|
2733
|
+
// De-duplicate by node identity and drop a block that is NESTED
|
|
2734
|
+
// inside another block. Some builds match both an outer message
|
|
2735
|
+
// container and an inner one; without this the restored dialogue
|
|
2736
|
+
// printed each turn twice. We keep only the OUTERMOST blocks.
|
|
2737
|
+
if (blocks.length > 1) {
|
|
2738
|
+
const unique = Array.from(new Set(blocks));
|
|
2739
|
+
blocks = unique.filter((b) => !unique.some((other) => other !== b && other.contains(b)));
|
|
2740
|
+
}
|
|
2464
2741
|
const out = [];
|
|
2465
2742
|
if (blocks.length) {
|
|
2466
2743
|
for (const b of blocks) {
|