zames_pro 2.90.15 → 2.90.18
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/CHANGELOG.md +34 -0
- package/dist/browser-chats.js +105 -64
- package/dist/browser-pure.js +28 -0
- package/dist/browser.js +137 -30
- package/dist/commands.js +1 -178
- package/dist/compact.js +42 -4
- package/dist/deepseek-ui.js +43 -7
- package/dist/glm-browser.js +23 -7
- package/dist/glm-chats.js +13 -1
- package/dist/glm-ui.js +4 -1
- package/dist/index.js +10 -6
- package/dist/net-capture.js +65 -7
- package/dist/qwen-browser.js +26 -7
- package/dist/qwen-chats.js +13 -1
- package/dist/qwen-net-capture.js +7 -1
- package/dist/qwen-ui.js +8 -1
- package/dist/restore-history.js +304 -0
- package/dist/tui/TuiEditor.js +8 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.90.18] - 2026-10-30
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- self-healing live interaction with the chat
|
|
15
|
+
- **restore:** drop the loop's timeout nudge
|
|
16
|
+
- **deepseek:** do not read a protocol marker mentioned in our own text
|
|
17
|
+
- **restore:** keep the loop's timeout nudge out of the restored dialogue
|
|
18
|
+
- **browser:** stop misreading a completed turn as truncated
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- drop the _netTurnBody and restore-nudge changes
|
|
23
|
+
|
|
24
|
+
## [2.90.17] - 2026-10-30
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- self-healing live interaction with the chat
|
|
29
|
+
- **restore:** drop the loop's timeout nudge
|
|
30
|
+
- **deepseek:** do not read a protocol marker mentioned in our own text
|
|
31
|
+
- **restore:** keep the loop's timeout nudge out of the restored dialogue
|
|
32
|
+
- **browser:** stop misreading a completed turn as truncated
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- drop the _netTurnBody and restore-nudge changes
|
|
37
|
+
|
|
38
|
+
## [2.90.16] - 2026-10-29
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **browser:** apply the sanitized headless UA to the relaunch explicitly
|
|
43
|
+
|
|
10
44
|
## [2.90.15] - 2026-10-29
|
|
11
45
|
|
|
12
46
|
### Changed
|
package/dist/browser-chats.js
CHANGED
|
@@ -119,68 +119,109 @@ export function scrapeChatList(limit) {
|
|
|
119
119
|
* builds match both an outer and an inner container, which printed each turn
|
|
120
120
|
* twice). The model's reasoning (think class) is skipped.
|
|
121
121
|
*/
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
122
|
+
// The in-page scraper is a SOURCE STRING compiled inside the page, not a
|
|
123
|
+
// function reference: tsx/esbuild injects a `__name(...)` helper into every
|
|
124
|
+
// NAMED function expression it transpiles, Playwright serializes that helper
|
|
125
|
+
// into the page, and the page has no such global — the whole scraper then threw
|
|
126
|
+
// `ReferenceError: __name is not defined` and the DOM fallback returned an
|
|
127
|
+
// EMPTY dialogue. Keeping the text here makes the in-page code explicit and
|
|
128
|
+
// testable (it is compiled with `new Function` by `scrapeChatMessagesInPage`).
|
|
129
|
+
const SCRAPE_CHAT_MESSAGES_SOURCE = [
|
|
130
|
+
'function scrapeChatMessages(opts) {',
|
|
131
|
+
" const think = new RegExp(opts.thinkRe, 'i')",
|
|
132
|
+
" const assistantRe = new RegExp(opts.assistantRe, 'i')",
|
|
133
|
+
' const inThink = (e) => {',
|
|
134
|
+
' let n = e',
|
|
135
|
+
' while (n) {',
|
|
136
|
+
" if (think.test((n.className || '').toString())) return true",
|
|
137
|
+
' n = n.parentElement',
|
|
138
|
+
' }',
|
|
139
|
+
' return false',
|
|
140
|
+
' }',
|
|
141
|
+
' const textOf = (e) => {',
|
|
142
|
+
" const t = e.innerText || e.textContent || ''",
|
|
143
|
+
' const NL = String.fromCharCode(10)',
|
|
144
|
+
" return t.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim()",
|
|
145
|
+
' }',
|
|
146
|
+
' // A REASONING block is UI state, not the answer, and it is not always a',
|
|
147
|
+
' // PARENT of the text: DeepSeek renders it as a sibling that precedes the',
|
|
148
|
+
' // markdown answer, so reading innerText printed the whole reasoning in the',
|
|
149
|
+
' // restored dialogue. Remove the reasoning SUBTREES, then read the rest.',
|
|
150
|
+
' const contentOf = (e) => {',
|
|
151
|
+
' // The reasoning text is INSIDE the message, so it must be removed',
|
|
152
|
+
' // before reading: the header line (Thought for N seconds) AND the',
|
|
153
|
+
' // body that follows it. The answer is the part AFTER the body.',
|
|
154
|
+
' const clone = e.cloneNode(true)',
|
|
155
|
+
' let hadThink = false',
|
|
156
|
+
" for (const t of Array.from(clone.querySelectorAll('*'))) {",
|
|
157
|
+
" if (think.test((t.className || '').toString())) {",
|
|
158
|
+
' hadThink = true',
|
|
159
|
+
' t.remove()',
|
|
160
|
+
' }',
|
|
161
|
+
' }',
|
|
162
|
+
' // The reasoning BODY can be a bare TEXT NODE (no element to remove),',
|
|
163
|
+
' // and a cloned node may even lose the block structure, so the text is',
|
|
164
|
+
' // taken from the ORIGINAL message and split into paragraphs: the',
|
|
165
|
+
' // reasoning is the first one, the answer is the last one.',
|
|
166
|
+
' const full = textOf(e)',
|
|
167
|
+
' const nl = String.fromCharCode(10)',
|
|
168
|
+
' const blocks = full.split(nl + nl).filter((p) => p.trim())',
|
|
169
|
+
" if (!hadThink && !/^(thought|thinking|think) for/.test(blocks[0] || '')) {",
|
|
170
|
+
' return full.trim()',
|
|
171
|
+
' }',
|
|
172
|
+
" const answer = blocks[blocks.length - 1] || ''",
|
|
173
|
+
" const hdr = new RegExp('^(thought|thinking|think) for[^' + nl + ']*' + nl + '*')",
|
|
174
|
+
" return answer.replace(hdr, '').trim()",
|
|
175
|
+
' }',
|
|
176
|
+
' // A USER bubble carries only the operator text; an assistant turn has a',
|
|
177
|
+
' // rendered markdown wrapper (live: div.ds-markdown with',
|
|
178
|
+
' // .ds-assistant-message-main-content).',
|
|
179
|
+
' const looksAssistant = (e) => {',
|
|
180
|
+
" if (assistantRe.test((e.className || '').toString())) return true",
|
|
181
|
+
' if (e.querySelector(opts.assistantSel)) return true',
|
|
182
|
+
' if (e.querySelector(opts.markdownSel)) return true',
|
|
183
|
+
' return false',
|
|
184
|
+
' }',
|
|
185
|
+
' let blocks = []',
|
|
186
|
+
' for (const s of opts.containerSels) {',
|
|
187
|
+
' const found = Array.from(document.querySelectorAll(s)).filter(',
|
|
188
|
+
' (e) => !inThink(e) && contentOf(e).length > 0,',
|
|
189
|
+
' )',
|
|
190
|
+
' if (found.length) {',
|
|
191
|
+
' blocks = found',
|
|
192
|
+
' break',
|
|
193
|
+
' }',
|
|
194
|
+
' }',
|
|
195
|
+
' if (blocks.length > 1) {',
|
|
196
|
+
' const unique = Array.from(new Set(blocks))',
|
|
197
|
+
' blocks = unique.filter(',
|
|
198
|
+
' (b) => !unique.some((other) => other !== b && other.contains(b)),',
|
|
199
|
+
' )',
|
|
200
|
+
' }',
|
|
201
|
+
' const out = []',
|
|
202
|
+
' if (blocks.length) {',
|
|
203
|
+
' for (const b of blocks) {',
|
|
204
|
+
' const t = contentOf(b)',
|
|
205
|
+
' if (!t) continue',
|
|
206
|
+
" out.push({ role: looksAssistant(b) ? 'assistant' : 'user', text: t })",
|
|
207
|
+
' }',
|
|
208
|
+
' if (out.length) return out',
|
|
209
|
+
' }',
|
|
210
|
+
' for (const s of opts.answerSels) {',
|
|
211
|
+
' const list = Array.from(document.querySelectorAll(s)).filter(',
|
|
212
|
+
' (e) => !inThink(e),',
|
|
213
|
+
' )',
|
|
214
|
+
' if (!list.length) continue',
|
|
215
|
+
' for (const el of list) {',
|
|
216
|
+
' const t = contentOf(el)',
|
|
217
|
+
" if (t) out.push({ role: 'assistant', text: t })",
|
|
218
|
+
' }',
|
|
219
|
+
' break',
|
|
220
|
+
' }',
|
|
221
|
+
' return out',
|
|
222
|
+
'}',
|
|
223
|
+
].join(String.fromCharCode(10));
|
|
224
|
+
/** The in-page scraper SOURCE (compiled inside the page, see above). */
|
|
225
|
+
export function scrapeChatMessagesSource() {
|
|
226
|
+
return SCRAPE_CHAT_MESSAGES_SOURCE;
|
|
186
227
|
}
|
package/dist/browser-pure.js
CHANGED
|
@@ -300,6 +300,18 @@ export function isRateLimitText(text) {
|
|
|
300
300
|
// start a new chat." and will refuse every further send into this chat. Distinct
|
|
301
301
|
// from the rate limit (which clears with time) and from a server hiccup — the
|
|
302
302
|
// only recovery is compaction, so ask() must not retry in the same chat.
|
|
303
|
+
/**
|
|
304
|
+
* A toast container can include the surrounding CONVERSATION, so a task that
|
|
305
|
+
* merely quotes "Length limit reached" (as the smoke script does while
|
|
306
|
+
* testing this detector) looked like a refused send and killed the run. A
|
|
307
|
+
* real notice is its OWN message: SHORT, and the phrase starts it.
|
|
308
|
+
*/
|
|
309
|
+
export function isContextLimitNotice(text) {
|
|
310
|
+
const t = String(text || '').trim();
|
|
311
|
+
if (!t || t.length > 400)
|
|
312
|
+
return false;
|
|
313
|
+
return isContextLimitText(t);
|
|
314
|
+
}
|
|
303
315
|
export function isContextLimitText(text) {
|
|
304
316
|
return CONTEXT_LIMIT_RE.test(String(text || ''));
|
|
305
317
|
}
|
|
@@ -383,3 +395,19 @@ export async function nukeProfile(dir = USER_DATA_DIR) {
|
|
|
383
395
|
}
|
|
384
396
|
catch { }
|
|
385
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* PURE: does this browser still need the headless User-Agent fix?
|
|
400
|
+
*
|
|
401
|
+
* `_fixHeadlessUserAgent()` bails out for an "already configured" UA — but
|
|
402
|
+
* using `if (this.userAgent) return` was wrong: the field is also the ENGINE's
|
|
403
|
+
* own UA captured from the page, and a config UA that merely CONTAINS the
|
|
404
|
+
* `HeadlessChrome` marker needs the fix just as much. The only value that can
|
|
405
|
+
* skip the fix is one that is already sanitized (and non-empty — an empty
|
|
406
|
+
* string means "ask the engine", not "use a blank UA").
|
|
407
|
+
*/
|
|
408
|
+
export function needsHeadlessUaFix(ua) {
|
|
409
|
+
const value = String(ua ?? '');
|
|
410
|
+
if (!value)
|
|
411
|
+
return true;
|
|
412
|
+
return sanitizeHeadlessUA(value) !== value;
|
|
413
|
+
}
|
package/dist/browser.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { chromium, } from 'playwright';
|
|
2
2
|
import { noteSwallowed } from './swallow.js';
|
|
3
3
|
import { interactiveBrowserLogin, loginIsInteractive, canShowWindow, } from './browser-login.js';
|
|
4
|
-
import { extractAnswer, extractTokenUsage, dumpNetBody, isFinishedWithoutAnswer, isFinishedWithAnswer, } from './net-capture.js';
|
|
4
|
+
import { extractAnswer, extractTokenUsage, dumpNetBody, isFinishedWithoutAnswer, isFinishedAwaitingAnswer, isFinishedWithAnswer, } from './net-capture.js';
|
|
5
5
|
import { rewriteFailedAttachments } from './commands.js';
|
|
6
6
|
import { recoverFromContextLimit } from './context-limit.js';
|
|
7
7
|
import { awaitUploadPreview, readAttachBuffers } from './attach-flow.js';
|
|
8
|
-
import { parseHistoryMessages, extractChatMessages, scrapeChatList,
|
|
8
|
+
import { parseHistoryMessages, extractChatMessages, scrapeChatList, scrapeChatMessagesSource, } from './browser-chats.js';
|
|
9
9
|
import { CHAT_URL, CHAT_ID_RE, chatUrl, HISTORY_MESSAGES_URL, BUTTON_CLASS_RE, INPUT_SELECTORS, SEND_SELECTORS, STOP_SELECTORS, STOP_NAME_RE, CONTINUE_NAME_RE, ANSWER_SELECTORS, THINK_CLASS_RE, MESSAGE_CONTAINER_SELECTORS, RESTORE_ANSWER_SELECTORS, ASSISTANT_CLASS_RE, ASSISTANT_SELECTOR, ANSWER_MARKDOWN_SELECTOR, MARKDOWN_SELECTOR, CHAT_SIGNAL_NODES_SELECTOR, NEW_CHAT_SELECTOR, NEW_CHAT_TEXT_RE, PASSWORD_SELECTORS, LOGIN_SELECTORS, LOGIN_SUBMIT_SELECTORS, MFA_CODE_SELECTORS, MFA_SUBMIT_SELECTORS, MFA_SEND_CODE_SELECTORS, MFA_SEND_CODE_RE, LOGIN_BUTTON_SELECTOR, LOGIN_NAME_RE, LOGIN_FORM_SUBMIT_RE, SIDEBAR_TOGGLE_SELECTORS, TOGGLE_SELECTORS, DEEP_THINKING_RE, WEB_SEARCH_RE, ATTACH_SELECTORS, ANSWER_URL_RE, } from './deepseek-ui.js';
|
|
10
10
|
import { probeUiHealth, } from './ui-health.js';
|
|
11
11
|
import { readPageToastsInDom, readLoginErrorInDom, readMfaAccountInDom, readLastAnswerInDom, chatSignalInDom, continueVisibleCheapInDom, stopButtonVisibleInDom, clickStopInDom, continueVisibleInDom, clickContinueInDom, readUserAgentInDom, findLoginInputIndexInDom, submitLoginFormInDom, setBusyTitleInDom, pasteTextIntoElement, readElementText, elementTagNameInDom, isElementEnabledInDom, readInputTrimmedTextInDom, dumpDomCountsInDom, } from './browser-dom.js';
|
|
@@ -23,7 +23,7 @@ export { STOP_NAME_RE, CONTINUE_NAME_RE };
|
|
|
23
23
|
// imported for the class below and RE-EXPORTED here, so the public API of this
|
|
24
24
|
// module is unchanged (tests and other modules import these names from
|
|
25
25
|
// './browser.js').
|
|
26
|
-
import { askPassword, USER_DATA_DIR, parseHeadlessUACache, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, headlessUACacheReady, sanitizeHeadlessUA, writeAuthMarker, isGenerationIncompleteText, isRateLimitText, isServerBusyText, isContextLimitText, normText, cleanAnswer, RateLimitError, ServerBusyError, GenerationIncompleteError, ContextLimitError, cleanSingletonFiles, profileLooksLocked, nukeProfile, } from './browser-pure.js';
|
|
26
|
+
import { askPassword, USER_DATA_DIR, parseHeadlessUACache, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, headlessUACacheReady, sanitizeHeadlessUA, needsHeadlessUaFix, writeAuthMarker, isGenerationIncompleteText, isRateLimitText, isServerBusyText, isContextLimitNotice, isContextLimitText, normText, cleanAnswer, RateLimitError, ServerBusyError, GenerationIncompleteError, ContextLimitError, cleanSingletonFiles, profileLooksLocked, nukeProfile, } from './browser-pure.js';
|
|
27
27
|
export { askPassword, parseHeadlessUACache, headlessUACacheReady, sanitizeHeadlessUA, isGenerationIncompleteText, isRateLimitText, isServerBusyText, isContextLimitText, normText, RateLimitError, ServerBusyError, GenerationIncompleteError, ContextLimitError, };
|
|
28
28
|
export class DeepSeekBrowser {
|
|
29
29
|
providerName = 'deepseek';
|
|
@@ -130,6 +130,7 @@ export class DeepSeekBrowser {
|
|
|
130
130
|
_apiPow;
|
|
131
131
|
// Last error from fetchChatMessages ('' on success). Surfaced under --debug
|
|
132
132
|
// so a missing restored history can be diagnosed.
|
|
133
|
+
/** Last error from fetchChatMessages ('' on success). */
|
|
133
134
|
_lastHistoryError;
|
|
134
135
|
// The RAW SSE body of the LAST answer response. Unlike `_netCapture`, this
|
|
135
136
|
// is kept even when the body carries no RESPONSE text (a truncated turn:
|
|
@@ -330,6 +331,12 @@ export class DeepSeekBrowser {
|
|
|
330
331
|
async _fixHeadlessUserAgent() {
|
|
331
332
|
if (!this.headless)
|
|
332
333
|
return;
|
|
334
|
+
// An already-sanitized UA (from the cache or a previous fix) needs no
|
|
335
|
+
// relaunch; an EMPTY field or a config UA that still carries the marker
|
|
336
|
+
// must be fixed, which the old blanket `if (this.userAgent) return` skipped
|
|
337
|
+
// on Qwen/GLM.
|
|
338
|
+
if (!needsHeadlessUaFix(this.userAgent))
|
|
339
|
+
return;
|
|
333
340
|
try {
|
|
334
341
|
const ua = await this.page.evaluate(readUserAgentInDom);
|
|
335
342
|
const fixed = sanitizeHeadlessUA(ua);
|
|
@@ -338,18 +345,28 @@ export class DeepSeekBrowser {
|
|
|
338
345
|
this.userAgent = fixed;
|
|
339
346
|
// Remember the good UA for this engine build so the NEXT headless start
|
|
340
347
|
// uses it immediately (no relaunch). Keyed by executable path.
|
|
341
|
-
|
|
348
|
+
await writeCachedHeadlessUA(headlessEngineKey(), fixed);
|
|
342
349
|
if (this.debug)
|
|
343
350
|
console.error('profile: headless UA -> relaunching with a plain Chrome UA');
|
|
344
351
|
await this.context.close().catch(() => { });
|
|
345
|
-
|
|
352
|
+
// Pass the UA EXPLICITLY: it was just sanitized, and relying on the field
|
|
353
|
+
// meant the relaunch could read the page's own UA back and keep the
|
|
354
|
+
// HeadlessChrome marker (a 403 / login wall from DeepSeek's CDN).
|
|
355
|
+
await this._launchOnce(fixed);
|
|
346
356
|
}
|
|
347
357
|
catch (e) {
|
|
348
358
|
if (this.debug)
|
|
349
359
|
console.error('profile: UA fix failed:', e.message);
|
|
350
360
|
}
|
|
351
361
|
}
|
|
352
|
-
|
|
362
|
+
/**
|
|
363
|
+
* Launch the persistent context. `uaOverride` applies a User-Agent to THIS
|
|
364
|
+
* launch without storing it first: the UA fix reads the engine UA from the
|
|
365
|
+
* page it is about to close, so passing the value explicitly is the only way
|
|
366
|
+
* the relaunch cannot fall back to the un-sanitized UA (a stored field was
|
|
367
|
+
* read by the OLD page and then re-read by the new one).
|
|
368
|
+
*/
|
|
369
|
+
async _launchOnce(uaOverride) {
|
|
353
370
|
const options = {
|
|
354
371
|
headless: this.headless,
|
|
355
372
|
// 30ms slowMo per Playwright action added up over thousands of DOM
|
|
@@ -360,8 +377,9 @@ export class DeepSeekBrowser {
|
|
|
360
377
|
};
|
|
361
378
|
if (this.channel)
|
|
362
379
|
options.channel = this.channel;
|
|
363
|
-
|
|
364
|
-
|
|
380
|
+
const ua = uaOverride || this.userAgent;
|
|
381
|
+
if (ua)
|
|
382
|
+
options.userAgent = ua;
|
|
365
383
|
try {
|
|
366
384
|
this.context = await chromium.launchPersistentContext(USER_DATA_DIR, options);
|
|
367
385
|
}
|
|
@@ -491,7 +509,7 @@ export class DeepSeekBrowser {
|
|
|
491
509
|
// `!extracted` guard skipped the check and the finish loop waited out
|
|
492
510
|
// the whole askDeadlineMs. Run the check on the RAW body regardless of
|
|
493
511
|
// whether a (bogus) fragment was extracted.
|
|
494
|
-
if (isFinishedWithoutAnswer(body)) {
|
|
512
|
+
if (isFinishedWithoutAnswer(body) && !isFinishedAwaitingAnswer(body)) {
|
|
495
513
|
// The UI shows Stopped + Continue. Record it with a fresh timestamp
|
|
496
514
|
// so the ask() loops do not wait out the timeout — they click
|
|
497
515
|
// Continue / resend.
|
|
@@ -2057,6 +2075,8 @@ export class DeepSeekBrowser {
|
|
|
2057
2075
|
report(gap);
|
|
2058
2076
|
this._emitSendState('paused');
|
|
2059
2077
|
await this._sleepInterruptible(gap, report);
|
|
2078
|
+
if (!this._abort && !this._askCancelled)
|
|
2079
|
+
this._emitSendState('settled');
|
|
2060
2080
|
}
|
|
2061
2081
|
// Sleep in small slices so Esc/Ctrl+C can cancel the wait promptly.
|
|
2062
2082
|
// Returns true if the sleep was cut short by _abort.
|
|
@@ -2251,15 +2271,22 @@ export class DeepSeekBrowser {
|
|
|
2251
2271
|
// Checked BEFORE the rate limit and the server hiccup: DeepSeek shows the
|
|
2252
2272
|
// context notice INSTEAD of an answer, and both of those branches would
|
|
2253
2273
|
// retry the same send into a chat that can never accept it again.
|
|
2254
|
-
|
|
2274
|
+
// A toast container can carry the TEXT of the conversation: a task
|
|
2275
|
+
// that merely MENTIONS "Length limit reached" (the smoke script does)
|
|
2276
|
+
// made the send look refused and every later turn died on the
|
|
2277
|
+
// compaction path. Only a notice that is its own message counts — see
|
|
2278
|
+
// isContextLimitNotice().
|
|
2279
|
+
if (isContextLimitNotice(pageText)) {
|
|
2255
2280
|
throw new ContextLimitError(pageText.slice(0, 300));
|
|
2256
2281
|
}
|
|
2257
2282
|
if (isRateLimitText(pageText)) {
|
|
2258
2283
|
throw new RateLimitError(pageText.slice(0, 300));
|
|
2259
2284
|
}
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2285
|
+
// NOT checked here: a server-busy NOTICE lives in a toast that can
|
|
2286
|
+
// carry the surrounding answer text, and a normal answer that merely
|
|
2287
|
+
// mentions "the server is busy" made this throw and RESENT a healthy
|
|
2288
|
+
// turn (live: 6 Continue clicks on one answer). The SSE body is the
|
|
2289
|
+
// authoritative place for it (_netServerBusy, see _onResponse).
|
|
2263
2290
|
// The rate limit may be visible ONLY in the SSE body (the DOM toast is
|
|
2264
2291
|
// short-lived and easy to miss). The old code threw ds.send_no_new_answer
|
|
2265
2292
|
// at the deadline while the chat plainly showed "Messages too frequent".
|
|
@@ -2273,7 +2300,29 @@ export class DeepSeekBrowser {
|
|
|
2273
2300
|
// truncated body has NO RESPONSE fragment, so extractAnswer() returned
|
|
2274
2301
|
// '' and `_netCapture` was empty — guarding on it made this branch dead
|
|
2275
2302
|
// code and the agent hung on "Stopped" until the timeout.
|
|
2276
|
-
|
|
2303
|
+
// A truncated turn whose first RESPONSE fragment is only a HEAD (a bare
|
|
2304
|
+
// `{` or a newline) is NOT dead: DeepSeek streams the answer and the rest
|
|
2305
|
+
// arrives in APPEND chunks a moment later, so keep waiting instead of
|
|
2306
|
+
// resending. Without this every DSML/XML answer produced the false
|
|
2307
|
+
// "DeepSeek оборвал ответ" message.
|
|
2308
|
+
const awaitingAnswer = this._netCaptureAt >= this._lastSentAt &&
|
|
2309
|
+
isFinishedAwaitingAnswer(this._netBody);
|
|
2310
|
+
if (awaitingAnswer) {
|
|
2311
|
+
this._askDebug('START-loop awaiting-answer (head-only snapshot)');
|
|
2312
|
+
}
|
|
2313
|
+
// A COMPLETE answer wins over a stale truncation marker: the model
|
|
2314
|
+
// may quote `generation_err` (our own detector source) or DeepSeek may
|
|
2315
|
+
// end the turn with a marker AFTER the answer arrived. Resending or
|
|
2316
|
+
// clicking Continue then LOSES a good answer (live: turn 10 repeated
|
|
2317
|
+
// turn 9 and hit the watchdog).
|
|
2318
|
+
const doneAnswer = this._netCaptureAt >= this._lastSentAt &&
|
|
2319
|
+
isFinishedWithAnswer(this._netBody);
|
|
2320
|
+
if (doneAnswer) {
|
|
2321
|
+
this._askDebug('START-loop FINISHED-with-answer, return');
|
|
2322
|
+
return this._cleanAnswer(this._netCapture);
|
|
2323
|
+
}
|
|
2324
|
+
if (!awaitingAnswer &&
|
|
2325
|
+
this._netCaptureAt >= this._lastSentAt &&
|
|
2277
2326
|
isGenerationIncompleteText(this._netBody)) {
|
|
2278
2327
|
// PREFER the Continue button: it RESUMES the same truncated turn (a
|
|
2279
2328
|
// `chat/continue` request). Resending the prompt creates a NEW turn and
|
|
@@ -2309,6 +2358,11 @@ export class DeepSeekBrowser {
|
|
|
2309
2358
|
this.deepThinking &&
|
|
2310
2359
|
(await this._clickContinueIfVisible())) {
|
|
2311
2360
|
this._askDebug('CLICKED Continue (start-loop)');
|
|
2361
|
+
// A click RESUMES the turn. Without the wait the next tick clicks
|
|
2362
|
+
// again the instant the button is still painted, and a "messages
|
|
2363
|
+
// too frequent" hits from our OWN burst of chat/continue requests.
|
|
2364
|
+
this._lastContinueAt = Date.now();
|
|
2365
|
+
await this.page.waitForTimeout(500);
|
|
2312
2366
|
}
|
|
2313
2367
|
const cur = await this._readLastAnswerTextCleanDom().catch(() => '');
|
|
2314
2368
|
// Cheap growth signal instead of the whole-body length.
|
|
@@ -2352,7 +2406,14 @@ export class DeepSeekBrowser {
|
|
|
2352
2406
|
const paused = this.autoContinue &&
|
|
2353
2407
|
this.deepThinking &&
|
|
2354
2408
|
(await this._continueButtonVisible());
|
|
2355
|
-
|
|
2409
|
+
// A head-only snapshot with no appended text yet is NOT settled: the
|
|
2410
|
+
// answer is still arriving, so a repeated DOM read must not be returned
|
|
2411
|
+
// as a finished answer (that was the "DeepSeek оборвал ответ" loop).
|
|
2412
|
+
if (!awaitingAnswer &&
|
|
2413
|
+
differs &&
|
|
2414
|
+
cur === lastStartCur &&
|
|
2415
|
+
notGenerating &&
|
|
2416
|
+
!paused) {
|
|
2356
2417
|
settledTicks++;
|
|
2357
2418
|
if (settledTicks >= 2) {
|
|
2358
2419
|
this._askDebug('SETTLED-differs return len=' + cur.length);
|
|
@@ -2474,9 +2535,9 @@ export class DeepSeekBrowser {
|
|
|
2474
2535
|
if (isRateLimitText(pageText)) {
|
|
2475
2536
|
throw new RateLimitError(pageText.slice(0, 300));
|
|
2476
2537
|
}
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2538
|
+
// The server-busy check is deliberately ABSENT: its toast can contain
|
|
2539
|
+
// the answer text, and a healthy answer that mentioned it threw here
|
|
2540
|
+
// (see the start-wait comment).
|
|
2480
2541
|
}
|
|
2481
2542
|
// The rate limit may exist only in the SSE body (the DOM toast is
|
|
2482
2543
|
// short-lived). Without this the loop waited out the deadline and threw
|
|
@@ -2496,10 +2557,30 @@ export class DeepSeekBrowser {
|
|
|
2496
2557
|
// and duplicates the work — only do that when there is no button to
|
|
2497
2558
|
// click. Runs on the RAW `_netBody` (a truncated body has no RESPONSE,
|
|
2498
2559
|
// so `_netCapture` is empty and the old guard was dead code).
|
|
2499
|
-
|
|
2560
|
+
// A COMPLETE answer is NOT truncated, whatever marker the body also
|
|
2561
|
+
// carries (a quoted `generation_err`, a late status): acting on it
|
|
2562
|
+
// would resend a healthy turn (live: a turn repeated the previous
|
|
2563
|
+
// answer and hit the watchdog).
|
|
2564
|
+
const doneAnswer = this._netCaptureAt >= this._lastSentAt &&
|
|
2565
|
+
isFinishedWithAnswer(this._netBody);
|
|
2566
|
+
const truncated = !doneAnswer &&
|
|
2567
|
+
this._netCaptureAt >= this._lastSentAt &&
|
|
2500
2568
|
isGenerationIncompleteText(this._netBody);
|
|
2501
|
-
const noAnswer =
|
|
2569
|
+
const noAnswer = !doneAnswer &&
|
|
2570
|
+
this._netNoAnswer &&
|
|
2571
|
+
this._netCaptureAt >= this._lastSentAt;
|
|
2502
2572
|
if ((truncated || noAnswer) && this.autoContinue) {
|
|
2573
|
+
// MEASURED: a COMPLETE DSML tool call ends with the event `close`
|
|
2574
|
+
// and `{click_behavior:'none', auto_resume:false}`, yet the DOM may
|
|
2575
|
+
// still paint a "Continue" control for a moment. Clicking it sends a
|
|
2576
|
+
// chat/continue request into an ENDED turn — the reply is "Messages
|
|
2577
|
+
// too frequent" and the answer is lost. If the SSE says FINISHED
|
|
2578
|
+
// WITH an answer, accept it first; Continue is for a turn that has
|
|
2579
|
+
// NOT finished.
|
|
2580
|
+
if (isFinishedWithAnswer(this._netBody)) {
|
|
2581
|
+
this._askDebug('SKIP Continue: FINISHED with answer');
|
|
2582
|
+
return this._cleanAnswer(this._netCapture);
|
|
2583
|
+
}
|
|
2503
2584
|
if (await this._clickContinueIfVisible()) {
|
|
2504
2585
|
this._askDebug('CLICKED Continue (finish-loop, truncated=' +
|
|
2505
2586
|
truncated +
|
|
@@ -2591,7 +2672,9 @@ export class DeepSeekBrowser {
|
|
|
2591
2672
|
if (this._netCaptureAt >= this._lastSentAt &&
|
|
2592
2673
|
isFinishedWithAnswer(this._netBody) === false &&
|
|
2593
2674
|
isGenerationIncompleteText(this._netBody) === false &&
|
|
2594
|
-
(this._netNoAnswer ||
|
|
2675
|
+
(this._netNoAnswer ||
|
|
2676
|
+
(isFinishedWithoutAnswer(this._netBody) &&
|
|
2677
|
+
!isFinishedAwaitingAnswer(this._netBody)))) {
|
|
2595
2678
|
deadTicks++;
|
|
2596
2679
|
if (deadTicks >= 6 && !(await this._continueButtonVisible())) {
|
|
2597
2680
|
this._askDebug('THROW dead-turn (finished without answer)');
|
|
@@ -2618,6 +2701,13 @@ export class DeepSeekBrowser {
|
|
|
2618
2701
|
(await this._continueButtonVisible())) {
|
|
2619
2702
|
if (await this._clickContinueIfVisible()) {
|
|
2620
2703
|
this._askDebug('CLICKED Continue (before early return)');
|
|
2704
|
+
// A model-generated tool call often ENDS with DeepSeek's own
|
|
2705
|
+
// 'click to continue' offer (a Continue button inside the
|
|
2706
|
+
// ANSWER), which is not a pause. Returning here is what the
|
|
2707
|
+
// loop wants; the old unconditional `continue` spun until the
|
|
2708
|
+
// watchdog and looked like a stalled agent.
|
|
2709
|
+
this._askDebug('RETURN stable-with-continue len=' + cur.length);
|
|
2710
|
+
return cur;
|
|
2621
2711
|
}
|
|
2622
2712
|
stable = 0;
|
|
2623
2713
|
}
|
|
@@ -2837,6 +2927,11 @@ export class DeepSeekBrowser {
|
|
|
2837
2927
|
};
|
|
2838
2928
|
}
|
|
2839
2929
|
}, { chatId: id, auth, pow, historyUrl: HISTORY_MESSAGES_URL })
|
|
2930
|
+
// A Playwright page TIMES OUT on its own (30s default) and the
|
|
2931
|
+
// in-page fetch has no timeout at all: on a rate-limited / hung chat
|
|
2932
|
+
// the promise never settles, so /compact (and /resume) waited forever
|
|
2933
|
+
// and reported a bare "unknown". Fail fast instead — the caller has a
|
|
2934
|
+
// local fallback.
|
|
2840
2935
|
.catch((e) => ({
|
|
2841
2936
|
error: 'evaluate failed: ' + e.message,
|
|
2842
2937
|
messages: null,
|
|
@@ -2879,22 +2974,34 @@ export class DeepSeekBrowser {
|
|
|
2879
2974
|
// bottom. Used by /resume and /resume-id so the operator sees the restored
|
|
2880
2975
|
// context in the terminal instead of just "Chat opened.".
|
|
2881
2976
|
//
|
|
2882
|
-
// Best-effort DOM scraping (the in-page scrapeChatMessages in
|
|
2883
|
-
// browser-chats.ts
|
|
2977
|
+
// Best-effort DOM scraping (the in-page scrapeChatMessages SOURCE in
|
|
2978
|
+
// browser-chats.ts — a source string, NOT a function reference, because tsx
|
|
2979
|
+
// injects a `__name` helper into a transpiled function and the page has no
|
|
2980
|
+
// such global, which silently emptied this fallback): role
|
|
2884
2981
|
// detection relies on the ds-assistant-message-* class, anything that is not
|
|
2885
2982
|
// an assistant block is a user message, and the model's reasoning
|
|
2886
2983
|
// (.ds-think-content) is skipped exactly like in _readLastAnswerText.
|
|
2887
2984
|
async readChatMessages() {
|
|
2888
2985
|
if (!this.page)
|
|
2889
2986
|
return [];
|
|
2987
|
+
// The scraper travels as SOURCE and is compiled inside the page: tsx
|
|
2988
|
+
// injects a `__name` helper into a transpiled function reference, the page
|
|
2989
|
+
// has no such global, and the whole fallback threw a ReferenceError (the
|
|
2990
|
+
// restored dialogue came back empty).
|
|
2890
2991
|
const raw = await this.page
|
|
2891
|
-
.evaluate(
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2992
|
+
.evaluate((payload) => {
|
|
2993
|
+
const factory = new Function(payload.source + '; return scrapeChatMessages');
|
|
2994
|
+
return factory()(payload.opts);
|
|
2995
|
+
}, {
|
|
2996
|
+
source: scrapeChatMessagesSource(),
|
|
2997
|
+
opts: {
|
|
2998
|
+
containerSels: MESSAGE_CONTAINER_SELECTORS,
|
|
2999
|
+
answerSels: RESTORE_ANSWER_SELECTORS,
|
|
3000
|
+
thinkRe: THINK_CLASS_RE.source,
|
|
3001
|
+
assistantRe: ASSISTANT_CLASS_RE.source,
|
|
3002
|
+
assistantSel: ASSISTANT_SELECTOR,
|
|
3003
|
+
markdownSel: ANSWER_MARKDOWN_SELECTOR,
|
|
3004
|
+
},
|
|
2898
3005
|
})
|
|
2899
3006
|
.catch(() => []);
|
|
2900
3007
|
return raw;
|