zames_pro 2.90.16 → 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 +28 -0
- package/dist/browser-chats.js +105 -64
- package/dist/browser-pure.js +12 -0
- package/dist/browser.js +115 -25
- package/dist/commands.js +1 -178
- package/dist/compact.js +42 -4
- package/dist/deepseek-ui.js +43 -7
- package/dist/glm-browser.js +14 -0
- 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 +11 -0
- 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,34 @@ 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
|
+
|
|
10
38
|
## [2.90.16] - 2026-10-29
|
|
11
39
|
|
|
12
40
|
### Fixed
|
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
|
}
|
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, needsHeadlessUaFix, 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:
|
|
@@ -508,7 +509,7 @@ export class DeepSeekBrowser {
|
|
|
508
509
|
// `!extracted` guard skipped the check and the finish loop waited out
|
|
509
510
|
// the whole askDeadlineMs. Run the check on the RAW body regardless of
|
|
510
511
|
// whether a (bogus) fragment was extracted.
|
|
511
|
-
if (isFinishedWithoutAnswer(body)) {
|
|
512
|
+
if (isFinishedWithoutAnswer(body) && !isFinishedAwaitingAnswer(body)) {
|
|
512
513
|
// The UI shows Stopped + Continue. Record it with a fresh timestamp
|
|
513
514
|
// so the ask() loops do not wait out the timeout — they click
|
|
514
515
|
// Continue / resend.
|
|
@@ -2074,6 +2075,8 @@ export class DeepSeekBrowser {
|
|
|
2074
2075
|
report(gap);
|
|
2075
2076
|
this._emitSendState('paused');
|
|
2076
2077
|
await this._sleepInterruptible(gap, report);
|
|
2078
|
+
if (!this._abort && !this._askCancelled)
|
|
2079
|
+
this._emitSendState('settled');
|
|
2077
2080
|
}
|
|
2078
2081
|
// Sleep in small slices so Esc/Ctrl+C can cancel the wait promptly.
|
|
2079
2082
|
// Returns true if the sleep was cut short by _abort.
|
|
@@ -2268,15 +2271,22 @@ export class DeepSeekBrowser {
|
|
|
2268
2271
|
// Checked BEFORE the rate limit and the server hiccup: DeepSeek shows the
|
|
2269
2272
|
// context notice INSTEAD of an answer, and both of those branches would
|
|
2270
2273
|
// retry the same send into a chat that can never accept it again.
|
|
2271
|
-
|
|
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)) {
|
|
2272
2280
|
throw new ContextLimitError(pageText.slice(0, 300));
|
|
2273
2281
|
}
|
|
2274
2282
|
if (isRateLimitText(pageText)) {
|
|
2275
2283
|
throw new RateLimitError(pageText.slice(0, 300));
|
|
2276
2284
|
}
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
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).
|
|
2280
2290
|
// The rate limit may be visible ONLY in the SSE body (the DOM toast is
|
|
2281
2291
|
// short-lived and easy to miss). The old code threw ds.send_no_new_answer
|
|
2282
2292
|
// at the deadline while the chat plainly showed "Messages too frequent".
|
|
@@ -2290,7 +2300,29 @@ export class DeepSeekBrowser {
|
|
|
2290
2300
|
// truncated body has NO RESPONSE fragment, so extractAnswer() returned
|
|
2291
2301
|
// '' and `_netCapture` was empty — guarding on it made this branch dead
|
|
2292
2302
|
// code and the agent hung on "Stopped" until the timeout.
|
|
2293
|
-
|
|
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 &&
|
|
2294
2326
|
isGenerationIncompleteText(this._netBody)) {
|
|
2295
2327
|
// PREFER the Continue button: it RESUMES the same truncated turn (a
|
|
2296
2328
|
// `chat/continue` request). Resending the prompt creates a NEW turn and
|
|
@@ -2326,6 +2358,11 @@ export class DeepSeekBrowser {
|
|
|
2326
2358
|
this.deepThinking &&
|
|
2327
2359
|
(await this._clickContinueIfVisible())) {
|
|
2328
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);
|
|
2329
2366
|
}
|
|
2330
2367
|
const cur = await this._readLastAnswerTextCleanDom().catch(() => '');
|
|
2331
2368
|
// Cheap growth signal instead of the whole-body length.
|
|
@@ -2369,7 +2406,14 @@ export class DeepSeekBrowser {
|
|
|
2369
2406
|
const paused = this.autoContinue &&
|
|
2370
2407
|
this.deepThinking &&
|
|
2371
2408
|
(await this._continueButtonVisible());
|
|
2372
|
-
|
|
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) {
|
|
2373
2417
|
settledTicks++;
|
|
2374
2418
|
if (settledTicks >= 2) {
|
|
2375
2419
|
this._askDebug('SETTLED-differs return len=' + cur.length);
|
|
@@ -2491,9 +2535,9 @@ export class DeepSeekBrowser {
|
|
|
2491
2535
|
if (isRateLimitText(pageText)) {
|
|
2492
2536
|
throw new RateLimitError(pageText.slice(0, 300));
|
|
2493
2537
|
}
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
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).
|
|
2497
2541
|
}
|
|
2498
2542
|
// The rate limit may exist only in the SSE body (the DOM toast is
|
|
2499
2543
|
// short-lived). Without this the loop waited out the deadline and threw
|
|
@@ -2513,10 +2557,30 @@ export class DeepSeekBrowser {
|
|
|
2513
2557
|
// and duplicates the work — only do that when there is no button to
|
|
2514
2558
|
// click. Runs on the RAW `_netBody` (a truncated body has no RESPONSE,
|
|
2515
2559
|
// so `_netCapture` is empty and the old guard was dead code).
|
|
2516
|
-
|
|
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 &&
|
|
2517
2568
|
isGenerationIncompleteText(this._netBody);
|
|
2518
|
-
const noAnswer =
|
|
2569
|
+
const noAnswer = !doneAnswer &&
|
|
2570
|
+
this._netNoAnswer &&
|
|
2571
|
+
this._netCaptureAt >= this._lastSentAt;
|
|
2519
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
|
+
}
|
|
2520
2584
|
if (await this._clickContinueIfVisible()) {
|
|
2521
2585
|
this._askDebug('CLICKED Continue (finish-loop, truncated=' +
|
|
2522
2586
|
truncated +
|
|
@@ -2608,7 +2672,9 @@ export class DeepSeekBrowser {
|
|
|
2608
2672
|
if (this._netCaptureAt >= this._lastSentAt &&
|
|
2609
2673
|
isFinishedWithAnswer(this._netBody) === false &&
|
|
2610
2674
|
isGenerationIncompleteText(this._netBody) === false &&
|
|
2611
|
-
(this._netNoAnswer ||
|
|
2675
|
+
(this._netNoAnswer ||
|
|
2676
|
+
(isFinishedWithoutAnswer(this._netBody) &&
|
|
2677
|
+
!isFinishedAwaitingAnswer(this._netBody)))) {
|
|
2612
2678
|
deadTicks++;
|
|
2613
2679
|
if (deadTicks >= 6 && !(await this._continueButtonVisible())) {
|
|
2614
2680
|
this._askDebug('THROW dead-turn (finished without answer)');
|
|
@@ -2635,6 +2701,13 @@ export class DeepSeekBrowser {
|
|
|
2635
2701
|
(await this._continueButtonVisible())) {
|
|
2636
2702
|
if (await this._clickContinueIfVisible()) {
|
|
2637
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;
|
|
2638
2711
|
}
|
|
2639
2712
|
stable = 0;
|
|
2640
2713
|
}
|
|
@@ -2854,6 +2927,11 @@ export class DeepSeekBrowser {
|
|
|
2854
2927
|
};
|
|
2855
2928
|
}
|
|
2856
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.
|
|
2857
2935
|
.catch((e) => ({
|
|
2858
2936
|
error: 'evaluate failed: ' + e.message,
|
|
2859
2937
|
messages: null,
|
|
@@ -2896,22 +2974,34 @@ export class DeepSeekBrowser {
|
|
|
2896
2974
|
// bottom. Used by /resume and /resume-id so the operator sees the restored
|
|
2897
2975
|
// context in the terminal instead of just "Chat opened.".
|
|
2898
2976
|
//
|
|
2899
|
-
// Best-effort DOM scraping (the in-page scrapeChatMessages in
|
|
2900
|
-
// 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
|
|
2901
2981
|
// detection relies on the ds-assistant-message-* class, anything that is not
|
|
2902
2982
|
// an assistant block is a user message, and the model's reasoning
|
|
2903
2983
|
// (.ds-think-content) is skipped exactly like in _readLastAnswerText.
|
|
2904
2984
|
async readChatMessages() {
|
|
2905
2985
|
if (!this.page)
|
|
2906
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).
|
|
2907
2991
|
const raw = await this.page
|
|
2908
|
-
.evaluate(
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
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
|
+
},
|
|
2915
3005
|
})
|
|
2916
3006
|
.catch(() => []);
|
|
2917
3007
|
return raw;
|