zames_pro 2.72.0 → 2.72.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/agent-loop.js +3 -5
- package/dist/browser-dom.js +416 -0
- package/dist/browser-pure.js +12 -1
- package/dist/browser.js +32 -325
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.72.1] - 2026-10-09
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **browser:** move cleanAnswer into browser-pure.ts (C3)
|
|
15
|
+
- **browser:** extract the element-level DOM callbacks into browser-dom.ts (C3)
|
|
16
|
+
- **browser:** extract the headless UA reader callback (C3)
|
|
17
|
+
- **browser:** extract the login-submit, UA-scan and busy-title callbacks (C3)
|
|
18
|
+
- **browser:** extract the in-page DOM callbacks into browser-dom.ts (C3)
|
|
19
|
+
- **answer-parse:** import the parser directly, drop the agent-loop re-export (C3b)
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **browser:** move cleanAnswer into browser-pure.ts (C3)
|
|
24
|
+
- **browser:** extract the element-level DOM callbacks into browser-dom.ts (C3)
|
|
25
|
+
- **browser:** extract the headless UA reader callback (C3)
|
|
26
|
+
- **browser:** extract the login-submit, UA-scan and busy-title callbacks (C3)
|
|
27
|
+
- **browser:** extract the in-page DOM callbacks into browser-dom.ts (C3)
|
|
28
|
+
- **answer-parse:** import the parser directly, drop the agent-loop re-export (C3b)
|
|
29
|
+
|
|
10
30
|
## [2.72.0] - 2026-10-08
|
|
11
31
|
|
|
12
32
|
### Added
|
package/dist/agent-loop.js
CHANGED
|
@@ -1090,9 +1090,7 @@ ${diag}`;
|
|
|
1090
1090
|
}
|
|
1091
1091
|
return 'Iteration limit reached.';
|
|
1092
1092
|
}
|
|
1093
|
-
// The pure helpers (tool-call parser + answer heuristics)
|
|
1094
|
-
// answer-parse.ts (BACKLOG C3)
|
|
1095
|
-
//
|
|
1096
|
-
// parseToolCall / responseLooksLikeToolCall / truncateToolResult from here).
|
|
1093
|
+
// The pure helpers (tool-call parser + answer heuristics) live in
|
|
1094
|
+
// answer-parse.ts (BACKLOG C3) and are imported here for runAgentLoop. They are
|
|
1095
|
+
// NOT re-exported: importers get them from answer-parse.js directly (C3b).
|
|
1097
1096
|
import { parseToolCall, responseLooksLikeToolCall, truncateToolResult, normForStale, looksLikeUnfinishedWork, isMeaningfulRespond, extractPreToolText, } from './answer-parse.js';
|
|
1098
|
-
export { parseToolCall, responseLooksLikeToolCall, truncateToolResult };
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
// Pure in-page DOM helpers extracted from DeepSeekBrowser (BACKLOG C3).
|
|
2
|
+
//
|
|
3
|
+
// Every function here runs INSIDE page.evaluate, so it MUST be fully
|
|
4
|
+
// self-contained: Playwright serializes the function SOURCE and evaluates it in
|
|
5
|
+
// the page, where module scope does not exist. Only DOM globals and the passed
|
|
6
|
+
// argument are available — a reference to another helper of this file would be
|
|
7
|
+
// lost. This is the same constraint browser-chats.ts works under; keeping the
|
|
8
|
+
// callbacks here instead of inline in the class keeps the class body thin
|
|
9
|
+
// without changing behavior.
|
|
10
|
+
/**
|
|
11
|
+
* Read ONLY visible toast/notification containers as one text blob.
|
|
12
|
+
*
|
|
13
|
+
* WHY this list is narrow: a broad `[class*="error" i]` matched leftover page
|
|
14
|
+
* text and produced a FALSE rate limit. Keeping to real toast containers means
|
|
15
|
+
* no false positives, so ask() does not go into a needless 5-minute wait.
|
|
16
|
+
*/
|
|
17
|
+
export function readPageToastsInDom() {
|
|
18
|
+
const sels = [
|
|
19
|
+
'[role="alert"]',
|
|
20
|
+
'[class*="toast" i]',
|
|
21
|
+
'[class*="notification" i]',
|
|
22
|
+
];
|
|
23
|
+
let out = '';
|
|
24
|
+
for (const s of sels) {
|
|
25
|
+
for (const el of Array.from(document.querySelectorAll(s))) {
|
|
26
|
+
const e = el;
|
|
27
|
+
const st = getComputedStyle(e);
|
|
28
|
+
if (st.display === 'none' || st.visibility === 'hidden')
|
|
29
|
+
continue;
|
|
30
|
+
out += ' ' + (e.innerText || '');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read a visible login/credentials error message from the page, if any.
|
|
37
|
+
* Best-effort: the caller slices/trims; this returns the joined text.
|
|
38
|
+
*/
|
|
39
|
+
export function readLoginErrorInDom() {
|
|
40
|
+
const sels = [
|
|
41
|
+
'[role="alert"]',
|
|
42
|
+
'[class*="error" i]',
|
|
43
|
+
'[class*="toast" i]',
|
|
44
|
+
'[class*="notification" i]',
|
|
45
|
+
];
|
|
46
|
+
let out = '';
|
|
47
|
+
for (const s of sels) {
|
|
48
|
+
for (const el of Array.from(document.querySelectorAll(s))) {
|
|
49
|
+
const e = el;
|
|
50
|
+
const st = getComputedStyle(e);
|
|
51
|
+
if (st.display === 'none' || st.visibility === 'hidden')
|
|
52
|
+
continue;
|
|
53
|
+
const t = (e.innerText || '').trim();
|
|
54
|
+
if (t)
|
|
55
|
+
out += ' ' + t;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out.replace(/\s+/g, ' ').trim();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The answer as RENDERED on the page, ALWAYS from the DOM (never the network
|
|
62
|
+
* capture). Reasoning blocks (think class) are skipped and must never be
|
|
63
|
+
* picked up as the answer.
|
|
64
|
+
*/
|
|
65
|
+
export function readLastAnswerInDom(opts) {
|
|
66
|
+
const sels = opts.sels;
|
|
67
|
+
const think = new RegExp(opts.thinkRe, 'i');
|
|
68
|
+
const inThink = (e) => {
|
|
69
|
+
let n = e;
|
|
70
|
+
while (n) {
|
|
71
|
+
const cls = (n.className || '').toString();
|
|
72
|
+
if (think.test(cls))
|
|
73
|
+
return true;
|
|
74
|
+
n = n.parentElement;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
};
|
|
78
|
+
let el = null;
|
|
79
|
+
for (const s of sels) {
|
|
80
|
+
const list = document.querySelectorAll(s);
|
|
81
|
+
if (!list.length)
|
|
82
|
+
continue;
|
|
83
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
84
|
+
const cand = list[i];
|
|
85
|
+
if (inThink(cand))
|
|
86
|
+
continue;
|
|
87
|
+
el = cand;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
if (el)
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (!el)
|
|
94
|
+
return '';
|
|
95
|
+
const out = el.innerText || el.textContent || '';
|
|
96
|
+
const NL = String.fromCharCode(10);
|
|
97
|
+
return out.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A CHEAP growth signal for the "did the answer start?" loop: the number of
|
|
101
|
+
* message/answer nodes plus the length of the LAST answer element. The old code
|
|
102
|
+
* read document.body.innerText.length every tick, which serializes the WHOLE
|
|
103
|
+
* page (sidebar, history, menus) and forces a layout — expensive on a long chat.
|
|
104
|
+
*/
|
|
105
|
+
export function chatSignalInDom(opts) {
|
|
106
|
+
const nodes = document.querySelectorAll(opts.nodesSel).length;
|
|
107
|
+
let lastLen = 0;
|
|
108
|
+
const answers = document.querySelectorAll(opts.mdSel);
|
|
109
|
+
if (answers.length) {
|
|
110
|
+
const lastEl = answers[answers.length - 1];
|
|
111
|
+
lastLen = (lastEl.innerText || '').length;
|
|
112
|
+
}
|
|
113
|
+
return { nodes, lastLen };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Cheap first probe for the Continue button: ONE scan of buttons/role-buttons
|
|
117
|
+
* for a matching visible label. The common path (no button) is probed every
|
|
118
|
+
* tick, so a single evaluate replaces the getByRole count+isVisible scan.
|
|
119
|
+
*/
|
|
120
|
+
export function continueVisibleCheapInDom(nameRe) {
|
|
121
|
+
const re = new RegExp(nameRe, 'i');
|
|
122
|
+
const cands = Array.from(document.querySelectorAll('div[role="button"], button'));
|
|
123
|
+
for (const e of cands) {
|
|
124
|
+
const label = ((e.textContent || '') +
|
|
125
|
+
' ' +
|
|
126
|
+
(e.getAttribute('aria-label') || ''))
|
|
127
|
+
.replace(/\s+/g, ' ')
|
|
128
|
+
.trim();
|
|
129
|
+
if (!re.test(label))
|
|
130
|
+
continue;
|
|
131
|
+
const st = getComputedStyle(e);
|
|
132
|
+
if (st.display === 'none' || st.visibility === 'hidden')
|
|
133
|
+
continue;
|
|
134
|
+
const r = e.getBoundingClientRect();
|
|
135
|
+
if (r.width > 0 && r.height > 0)
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Find the Stop button in the DeepSeek UI. We can't rely on the class alone:
|
|
142
|
+
* during generation the send button (the same circle button) changes its icon
|
|
143
|
+
* to a "square" (stop) while keeping the classes, so a square icon (svg rect)
|
|
144
|
+
* also means Stop.
|
|
145
|
+
*/
|
|
146
|
+
export function stopButtonVisibleInDom(btnRe) {
|
|
147
|
+
const re = new RegExp(btnRe, 'i');
|
|
148
|
+
const btns = Array.from(document.querySelectorAll('div[role="button"], button'));
|
|
149
|
+
for (const b of btns) {
|
|
150
|
+
const cls = (b.className || '').toString();
|
|
151
|
+
if (!re.test(cls))
|
|
152
|
+
continue;
|
|
153
|
+
const label = ((b.getAttribute('aria-label') || '') +
|
|
154
|
+
' ' +
|
|
155
|
+
(b.getAttribute('title') || '') +
|
|
156
|
+
' ' +
|
|
157
|
+
(b.textContent || '')).toLowerCase();
|
|
158
|
+
if (/stop|останов/.test(label))
|
|
159
|
+
return true;
|
|
160
|
+
// A square icon = Stop button; an arrow (path without rect) = send.
|
|
161
|
+
const svg = b.querySelector('svg');
|
|
162
|
+
if (svg && svg.querySelector('rect'))
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Raw-DOM Stop click with the full pointer/mouse sequence (older builds
|
|
169
|
+
* without a proper role, or a click intercepted by an overlay). Returns true
|
|
170
|
+
* when a square-icon button was found and clicked.
|
|
171
|
+
*/
|
|
172
|
+
export function clickStopInDom(btnRe) {
|
|
173
|
+
const re = new RegExp(btnRe, 'i');
|
|
174
|
+
const btns = Array.from(document.querySelectorAll('div[role="button"], button'));
|
|
175
|
+
for (const b of btns) {
|
|
176
|
+
const cls = (b.className || '').toString();
|
|
177
|
+
if (!re.test(cls))
|
|
178
|
+
continue;
|
|
179
|
+
const svg = b.querySelector('svg');
|
|
180
|
+
if (svg && svg.querySelector('rect')) {
|
|
181
|
+
for (const type of [
|
|
182
|
+
'pointerdown',
|
|
183
|
+
'mousedown',
|
|
184
|
+
'pointerup',
|
|
185
|
+
'mouseup',
|
|
186
|
+
'click',
|
|
187
|
+
]) {
|
|
188
|
+
b.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true }));
|
|
189
|
+
}
|
|
190
|
+
b.click();
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Raw-DOM scan for a visible Continue button (fallback for builds without a
|
|
198
|
+
* proper role). The label match is an EXACT short form (optionally with a
|
|
199
|
+
* reasoning suffix) so a "Continue" inside rendered prose is never matched.
|
|
200
|
+
*/
|
|
201
|
+
export function continueVisibleInDom() {
|
|
202
|
+
const labelOf = (b) => ((b.textContent || '') + ' ' + (b.getAttribute('aria-label') || ''))
|
|
203
|
+
.replace(/\s+/g, ' ')
|
|
204
|
+
.trim();
|
|
205
|
+
const isExact = (t) => /^(?:continue|продолжить|продолжение)(?:\s+(?:think(?:ing)?|reason(?:ing)?|размышлени[ея]|генераци[юя]|ответ))?\s*[.!…]?$/i.test(t);
|
|
206
|
+
const cands = Array.from(document.querySelectorAll('div[role="button"], button'));
|
|
207
|
+
for (const e of cands) {
|
|
208
|
+
if (!isExact(labelOf(e)))
|
|
209
|
+
continue;
|
|
210
|
+
const st = getComputedStyle(e);
|
|
211
|
+
if (st.display === 'none' || st.visibility === 'hidden')
|
|
212
|
+
continue;
|
|
213
|
+
const r = e.getBoundingClientRect();
|
|
214
|
+
if (r.width > 0 && r.height > 0)
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Raw-DOM Continue click with the full pointer/mouse sequence (fallback for a
|
|
221
|
+
* build without a proper role, or a click intercepted by an overlay).
|
|
222
|
+
* Returns true when a matching visible button was clicked.
|
|
223
|
+
*/
|
|
224
|
+
export function clickContinueInDom() {
|
|
225
|
+
const labelOf = (b) => ((b.textContent || '') + ' ' + (b.getAttribute('aria-label') || ''))
|
|
226
|
+
.replace(/\s+/g, ' ')
|
|
227
|
+
.trim();
|
|
228
|
+
const isExact = (t) => /^(?:continue|продолжить|продолжение)(?:\s+(?:think(?:ing)?|reason(?:ing)?|размышлени[ея]|генераци[юя]|ответ))?\s*[.!…]?$/i.test(t);
|
|
229
|
+
const cands = Array.from(document.querySelectorAll('div[role="button"], button'));
|
|
230
|
+
for (const e of cands) {
|
|
231
|
+
const t = labelOf(e);
|
|
232
|
+
if (!isExact(t))
|
|
233
|
+
continue;
|
|
234
|
+
const st = getComputedStyle(e);
|
|
235
|
+
if (st.display === 'none' || st.visibility === 'hidden')
|
|
236
|
+
continue;
|
|
237
|
+
const rect = e.getBoundingClientRect();
|
|
238
|
+
if (rect.width === 0 || rect.height === 0)
|
|
239
|
+
continue;
|
|
240
|
+
const cx = rect.left + rect.width / 2;
|
|
241
|
+
const cy = rect.top + rect.height / 2;
|
|
242
|
+
const opts = {
|
|
243
|
+
bubbles: true,
|
|
244
|
+
cancelable: true,
|
|
245
|
+
composed: true,
|
|
246
|
+
clientX: cx,
|
|
247
|
+
clientY: cy,
|
|
248
|
+
button: 0,
|
|
249
|
+
};
|
|
250
|
+
try {
|
|
251
|
+
e.dispatchEvent(new PointerEvent('pointerdown', opts));
|
|
252
|
+
e.dispatchEvent(new MouseEvent('mousedown', opts));
|
|
253
|
+
e.dispatchEvent(new PointerEvent('pointerup', opts));
|
|
254
|
+
e.dispatchEvent(new MouseEvent('mouseup', opts));
|
|
255
|
+
}
|
|
256
|
+
catch { }
|
|
257
|
+
e.click();
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Insert text into a contenteditable/input element by dispatching a synthetic
|
|
264
|
+
* paste event (the way a real paste lands, which React handlers honor). Runs
|
|
265
|
+
* bound to the element, so it receives `el` + the text value.
|
|
266
|
+
*/
|
|
267
|
+
export function pasteTextIntoElement(el, value) {
|
|
268
|
+
el.focus();
|
|
269
|
+
const dt = new DataTransfer();
|
|
270
|
+
dt.setData('text/plain', value);
|
|
271
|
+
const ev = new ClipboardEvent('paste', {
|
|
272
|
+
clipboardData: dt,
|
|
273
|
+
bubbles: true,
|
|
274
|
+
cancelable: true,
|
|
275
|
+
});
|
|
276
|
+
el.dispatchEvent(ev);
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Read the current text of an input/textarea (its `value`) or a
|
|
281
|
+
* contenteditable element (its innerText/textContent). Runs bound to the
|
|
282
|
+
* element so it can distinguish the two by tagName.
|
|
283
|
+
*/
|
|
284
|
+
export function readElementText(el) {
|
|
285
|
+
const tag = el.tagName.toLowerCase();
|
|
286
|
+
if (tag === 'textarea' || tag === 'input') {
|
|
287
|
+
return el.value;
|
|
288
|
+
}
|
|
289
|
+
return el.innerText || el.textContent || '';
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Lowercased tagName of an element. Runs bound to the element (Playwright's
|
|
293
|
+
* Locator.evaluate), used to tell a native input/textarea from a
|
|
294
|
+
* contenteditable box.
|
|
295
|
+
*/
|
|
296
|
+
export function elementTagNameInDom(el) {
|
|
297
|
+
return el.tagName.toLowerCase();
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Is the send control actually enabled? DeepSeek's send control is a
|
|
301
|
+
* div[role=button]; Playwright's isEnabled() does NOT honor aria-disabled on a
|
|
302
|
+
* div, and a click on a disabled button is silently ignored (during an upload
|
|
303
|
+
* the message stays in the box). Read the attribute and a disabled class
|
|
304
|
+
* directly. Runs bound to the element.
|
|
305
|
+
*/
|
|
306
|
+
export function isElementEnabledInDom(el) {
|
|
307
|
+
if (el.getAttribute('aria-disabled') === 'true')
|
|
308
|
+
return false;
|
|
309
|
+
const cls = (el.className || '').toString();
|
|
310
|
+
if (/disabled|is-disabled/.test(cls))
|
|
311
|
+
return false;
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* The current text of the input box, trimmed — used to verify the message was
|
|
316
|
+
* actually SENT (DeepSeek clears the box on accept; if the text is still there
|
|
317
|
+
* the click/Enter was ignored). Runs bound to the element.
|
|
318
|
+
*/
|
|
319
|
+
export function readInputTrimmedTextInDom(el) {
|
|
320
|
+
return (el.value || el.innerText || el.textContent || '').trim();
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* The real User-Agent the engine reports. Read after launch so a headless run
|
|
324
|
+
* can strip the "Headless" marker (DeepSeek's CDN 403s that UA).
|
|
325
|
+
*/
|
|
326
|
+
export function readUserAgentInDom() {
|
|
327
|
+
return navigator.userAgent;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Find the index of the text/email/tel input that immediately precedes the
|
|
331
|
+
* password field, so the login form can be filled when no explicit
|
|
332
|
+
* LOGIN_SELECTORS matched. Returns -1 when not found.
|
|
333
|
+
*/
|
|
334
|
+
export function findLoginInputIndexInDom(pwdSel) {
|
|
335
|
+
const p = document.querySelector(pwdSel);
|
|
336
|
+
if (!p)
|
|
337
|
+
return -1;
|
|
338
|
+
const inputs = Array.from(document.querySelectorAll('input'));
|
|
339
|
+
const pi = inputs.indexOf(p);
|
|
340
|
+
for (let i = pi - 1; i >= 0; i--) {
|
|
341
|
+
const t = (inputs[i].type || 'text').toLowerCase();
|
|
342
|
+
if (t === 'text' || t === 'email' || t === 'tel')
|
|
343
|
+
return i;
|
|
344
|
+
}
|
|
345
|
+
return -1;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Submit the login form from inside the page: click the button whose label
|
|
349
|
+
* matches the login regex, else the primary-looking button in the password
|
|
350
|
+
* field's form. Returns true when a button was clicked. The in-page click is
|
|
351
|
+
* a fallback for a React form whose submit control the selectors missed.
|
|
352
|
+
*/
|
|
353
|
+
export function submitLoginFormInDom(opts) {
|
|
354
|
+
const loginRe = new RegExp(opts.submitRe, 'i');
|
|
355
|
+
const p = document.querySelector(opts.pwdSel);
|
|
356
|
+
if (!p)
|
|
357
|
+
return false;
|
|
358
|
+
const form = p.closest('form');
|
|
359
|
+
const scope = form || document;
|
|
360
|
+
const btns = Array.from(scope.querySelectorAll('button[type="submit"], button, div[role="button"]'));
|
|
361
|
+
for (const b of btns) {
|
|
362
|
+
const txt = (b.textContent || '').trim().toLowerCase();
|
|
363
|
+
const aria = (b.getAttribute('aria-label') || '').toLowerCase();
|
|
364
|
+
if (loginRe.test(txt + ' ' + aria)) {
|
|
365
|
+
b.click();
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Fallback: the primary-looking button in the scope.
|
|
370
|
+
const primary = btns.find((b) => {
|
|
371
|
+
const cls = (b.className || '').toString();
|
|
372
|
+
return /primary|submit|ds-button--primary/i.test(cls);
|
|
373
|
+
});
|
|
374
|
+
if (primary) {
|
|
375
|
+
primary.click();
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Prefix/restore the tab title so the operator, looking at the browser
|
|
382
|
+
* window, can tell that the agent is generating. The ORIGINAL title is saved
|
|
383
|
+
* ONCE on `window` and restored when generation ends, so DeepSeek's own chat
|
|
384
|
+
* title is never lost.
|
|
385
|
+
*/
|
|
386
|
+
export function setBusyTitleInDom(on) {
|
|
387
|
+
const w = window;
|
|
388
|
+
if (on) {
|
|
389
|
+
if (typeof w.__zamesTitle !== 'string') {
|
|
390
|
+
w.__zamesTitle = document.title.replace(/^⏳\s*/, '');
|
|
391
|
+
}
|
|
392
|
+
document.title = '⏳ ' + w.__zamesTitle;
|
|
393
|
+
}
|
|
394
|
+
else if (typeof w.__zamesTitle === 'string') {
|
|
395
|
+
document.title = w.__zamesTitle;
|
|
396
|
+
w.__zamesTitle = undefined;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Count how many elements each candidate selector matches, for /debug-dom.
|
|
401
|
+
* Runs in-page (see the module header) so it takes the selector lists as an
|
|
402
|
+
* argument instead of referencing deepseek-ui.ts.
|
|
403
|
+
*/
|
|
404
|
+
export function dumpDomCountsInDom(sels) {
|
|
405
|
+
const result = { answers: {}, stops: {}, inputs: {} };
|
|
406
|
+
for (const s of sels.answers) {
|
|
407
|
+
result.answers[s] = document.querySelectorAll(s).length;
|
|
408
|
+
}
|
|
409
|
+
for (const s of sels.stops) {
|
|
410
|
+
result.stops[s] = document.querySelectorAll(s).length;
|
|
411
|
+
}
|
|
412
|
+
for (const s of sels.inputs) {
|
|
413
|
+
result.inputs[s] = document.querySelectorAll(s).length;
|
|
414
|
+
}
|
|
415
|
+
return result;
|
|
416
|
+
}
|
package/dist/browser-pure.js
CHANGED
|
@@ -2,7 +2,18 @@ import path from 'path';
|
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
import { chromium } from 'playwright';
|
|
5
|
-
import { GENERATION_ERR_RE, INCOMPLETE_STATUS_RE, RATE_LIMIT_RE, SERVER_BUSY_RE, } from './deepseek-ui.js';
|
|
5
|
+
import { GENERATION_ERR_RE, INCOMPLETE_STATUS_RE, RATE_LIMIT_RE, SERVER_BUSY_RE, STATUS_RE, } from './deepseek-ui.js';
|
|
6
|
+
// Drop service placeholders ("Reading…", "Thinking…") so they are not mistaken
|
|
7
|
+
// for an answer; keep everything else as-is (including the whitespace the
|
|
8
|
+
// tool-call relies on). Pure: no `this`, no page.
|
|
9
|
+
export function cleanAnswer(raw) {
|
|
10
|
+
const t = (raw || '').trim();
|
|
11
|
+
if (!t)
|
|
12
|
+
return '';
|
|
13
|
+
if (STATUS_RE.test(t))
|
|
14
|
+
return '';
|
|
15
|
+
return raw;
|
|
16
|
+
}
|
|
6
17
|
// Pure, state-free helpers extracted from browser.ts (BACKLOG C3). Nothing
|
|
7
18
|
// here touches the DeepSeekBrowser instance state, so this module is safe to
|
|
8
19
|
// import anywhere (including hot-reloaded modules) without pulling in the
|
package/dist/browser.js
CHANGED
|
@@ -2,8 +2,9 @@ import { chromium, } from 'playwright';
|
|
|
2
2
|
import { extractAnswer, extractTokenUsage, dumpNetBody, isFinishedWithoutAnswer, } from './net-capture.js';
|
|
3
3
|
import { rewriteFailedAttachments } from './commands.js';
|
|
4
4
|
import { parseHistoryMessages, extractChatMessages, scrapeChatList, scrapeChatMessages, } from './browser-chats.js';
|
|
5
|
-
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, LOGIN_BUTTON_SELECTOR, LOGIN_NAME_RE, LOGIN_FORM_SUBMIT_RE, SIDEBAR_TOGGLE_SELECTORS, TOGGLE_SELECTORS, DEEP_THINKING_RE, WEB_SEARCH_RE, ATTACH_SELECTORS,
|
|
5
|
+
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, 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';
|
|
6
6
|
import { probeUiHealth, } from './ui-health.js';
|
|
7
|
+
import { readPageToastsInDom, readLoginErrorInDom, readLastAnswerInDom, chatSignalInDom, continueVisibleCheapInDom, stopButtonVisibleInDom, clickStopInDom, continueVisibleInDom, clickContinueInDom, readUserAgentInDom, findLoginInputIndexInDom, submitLoginFormInDom, setBusyTitleInDom, pasteTextIntoElement, readElementText, elementTagNameInDom, isElementEnabledInDom, readInputTrimmedTextInDom, dumpDomCountsInDom, } from './browser-dom.js';
|
|
7
8
|
import path from 'path';
|
|
8
9
|
import os from 'os';
|
|
9
10
|
import fs from 'fs/promises';
|
|
@@ -18,7 +19,7 @@ export { STOP_NAME_RE, CONTINUE_NAME_RE };
|
|
|
18
19
|
// imported for the class below and RE-EXPORTED here, so the public API of this
|
|
19
20
|
// module is unchanged (tests and other modules import these names from
|
|
20
21
|
// './browser.js').
|
|
21
|
-
import { askPassword, USER_DATA_DIR, parseHeadlessUACache, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, headlessUACacheReady, sanitizeHeadlessUA, AUTH_MARKER_FILE, isGenerationIncompleteText, isRateLimitText, isServerBusyText, normText, RateLimitError, ServerBusyError, GenerationIncompleteError, cleanSingletonFiles, profileLooksLocked, nukeProfile, } from './browser-pure.js';
|
|
22
|
+
import { askPassword, USER_DATA_DIR, parseHeadlessUACache, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, headlessUACacheReady, sanitizeHeadlessUA, AUTH_MARKER_FILE, isGenerationIncompleteText, isRateLimitText, isServerBusyText, normText, cleanAnswer, RateLimitError, ServerBusyError, GenerationIncompleteError, cleanSingletonFiles, profileLooksLocked, nukeProfile, } from './browser-pure.js';
|
|
22
23
|
export { askPassword, parseHeadlessUACache, headlessUACacheReady, sanitizeHeadlessUA, isGenerationIncompleteText, isRateLimitText, isServerBusyText, normText, RateLimitError, ServerBusyError, GenerationIncompleteError, };
|
|
23
24
|
export class DeepSeekBrowser {
|
|
24
25
|
headless;
|
|
@@ -257,19 +258,7 @@ export class DeepSeekBrowser {
|
|
|
257
258
|
// the send loop.
|
|
258
259
|
async _setBusyTitle(busy) {
|
|
259
260
|
try {
|
|
260
|
-
await this.page.evaluate(
|
|
261
|
-
const w = window;
|
|
262
|
-
if (on) {
|
|
263
|
-
if (typeof w.__zamesTitle !== 'string') {
|
|
264
|
-
w.__zamesTitle = document.title.replace(/^⏳\s*/, '');
|
|
265
|
-
}
|
|
266
|
-
document.title = '⏳ ' + w.__zamesTitle;
|
|
267
|
-
}
|
|
268
|
-
else if (typeof w.__zamesTitle === 'string') {
|
|
269
|
-
document.title = w.__zamesTitle;
|
|
270
|
-
w.__zamesTitle = undefined;
|
|
271
|
-
}
|
|
272
|
-
}, busy);
|
|
261
|
+
await this.page.evaluate(setBusyTitleInDom, busy);
|
|
273
262
|
}
|
|
274
263
|
catch { }
|
|
275
264
|
}
|
|
@@ -308,7 +297,7 @@ export class DeepSeekBrowser {
|
|
|
308
297
|
if (!this.headless)
|
|
309
298
|
return;
|
|
310
299
|
try {
|
|
311
|
-
const ua = await this.page.evaluate(
|
|
300
|
+
const ua = await this.page.evaluate(readUserAgentInDom);
|
|
312
301
|
const fixed = sanitizeHeadlessUA(ua);
|
|
313
302
|
if (fixed === ua)
|
|
314
303
|
return;
|
|
@@ -641,27 +630,7 @@ export class DeepSeekBrowser {
|
|
|
641
630
|
*/
|
|
642
631
|
async _readLoginError() {
|
|
643
632
|
try {
|
|
644
|
-
const txt = await this.page.evaluate(
|
|
645
|
-
const sels = [
|
|
646
|
-
'[role="alert"]',
|
|
647
|
-
'[class*="error" i]',
|
|
648
|
-
'[class*="toast" i]',
|
|
649
|
-
'[class*="notification" i]',
|
|
650
|
-
];
|
|
651
|
-
let out = '';
|
|
652
|
-
for (const s of sels) {
|
|
653
|
-
for (const el of Array.from(document.querySelectorAll(s))) {
|
|
654
|
-
const e = el;
|
|
655
|
-
const st = getComputedStyle(e);
|
|
656
|
-
if (st.display === 'none' || st.visibility === 'hidden')
|
|
657
|
-
continue;
|
|
658
|
-
const t = (e.innerText || '').trim();
|
|
659
|
-
if (t)
|
|
660
|
-
out += ' ' + t;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
return out.replace(/\s+/g, ' ').trim();
|
|
664
|
-
});
|
|
633
|
+
const txt = await this.page.evaluate(readLoginErrorInDom);
|
|
665
634
|
return txt.slice(0, 200);
|
|
666
635
|
}
|
|
667
636
|
catch {
|
|
@@ -690,19 +659,7 @@ export class DeepSeekBrowser {
|
|
|
690
659
|
if (!userInput) {
|
|
691
660
|
// Find the index of the password input and use the closest preceding
|
|
692
661
|
// text/email/tel input as the login field.
|
|
693
|
-
const idx = await this.page.evaluate(
|
|
694
|
-
const p = document.querySelector(pwdSel);
|
|
695
|
-
if (!p)
|
|
696
|
-
return -1;
|
|
697
|
-
const inputs = Array.from(document.querySelectorAll('input'));
|
|
698
|
-
const pi = inputs.indexOf(p);
|
|
699
|
-
for (let i = pi - 1; i >= 0; i--) {
|
|
700
|
-
const t = (inputs[i].type || 'text').toLowerCase();
|
|
701
|
-
if (t === 'text' || t === 'email' || t === 'tel')
|
|
702
|
-
return i;
|
|
703
|
-
}
|
|
704
|
-
return -1;
|
|
705
|
-
}, PASSWORD_SELECTORS[0]);
|
|
662
|
+
const idx = await this.page.evaluate(findLoginInputIndexInDom, PASSWORD_SELECTORS[0]);
|
|
706
663
|
if (idx >= 0)
|
|
707
664
|
userInput = this.page.locator('input').nth(idx);
|
|
708
665
|
}
|
|
@@ -756,33 +713,7 @@ export class DeepSeekBrowser {
|
|
|
756
713
|
}
|
|
757
714
|
// Button inside the same form as the password field.
|
|
758
715
|
const formClicked = await this.page
|
|
759
|
-
.evaluate(
|
|
760
|
-
const loginRe = new RegExp(opts.submitRe, 'i');
|
|
761
|
-
const p = document.querySelector(opts.pwdSel);
|
|
762
|
-
if (!p)
|
|
763
|
-
return false;
|
|
764
|
-
const form = p.closest('form');
|
|
765
|
-
const scope = form || document;
|
|
766
|
-
const btns = Array.from(scope.querySelectorAll('button[type="submit"], button, div[role="button"]'));
|
|
767
|
-
for (const b of btns) {
|
|
768
|
-
const txt = (b.textContent || '').trim().toLowerCase();
|
|
769
|
-
const aria = (b.getAttribute('aria-label') || '').toLowerCase();
|
|
770
|
-
if (loginRe.test(txt + ' ' + aria)) {
|
|
771
|
-
b.click();
|
|
772
|
-
return true;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
// Fallback: the primary-looking button in the scope.
|
|
776
|
-
const primary = btns.find((b) => {
|
|
777
|
-
const cls = (b.className || '').toString();
|
|
778
|
-
return /primary|submit|ds-button--primary/i.test(cls);
|
|
779
|
-
});
|
|
780
|
-
if (primary) {
|
|
781
|
-
primary.click();
|
|
782
|
-
return true;
|
|
783
|
-
}
|
|
784
|
-
return false;
|
|
785
|
-
}, {
|
|
716
|
+
.evaluate(submitLoginFormInDom, {
|
|
786
717
|
pwdSel: PASSWORD_SELECTORS[0],
|
|
787
718
|
submitRe: LOGIN_FORM_SUBMIT_RE.source,
|
|
788
719
|
})
|
|
@@ -936,44 +867,10 @@ export class DeepSeekBrowser {
|
|
|
936
867
|
// call". The DOM reader has no such self-comparison problem: before the
|
|
937
868
|
// send the DOM shows the OLD answer, after it the NEW one.
|
|
938
869
|
async _readLastAnswerTextDom() {
|
|
939
|
-
return await this.page.evaluate(
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
// (otherwise the terminal would show the long reasoning). We also
|
|
944
|
-
// prefer the first (most specific) selector with a hit.
|
|
945
|
-
const think = new RegExp(opts.thinkRe, 'i');
|
|
946
|
-
const inThink = (e) => {
|
|
947
|
-
let n = e;
|
|
948
|
-
while (n) {
|
|
949
|
-
const cls = (n.className || '').toString();
|
|
950
|
-
if (think.test(cls))
|
|
951
|
-
return true;
|
|
952
|
-
n = n.parentElement;
|
|
953
|
-
}
|
|
954
|
-
return false;
|
|
955
|
-
};
|
|
956
|
-
let el = null;
|
|
957
|
-
for (const s of sels) {
|
|
958
|
-
const list = document.querySelectorAll(s);
|
|
959
|
-
if (!list.length)
|
|
960
|
-
continue;
|
|
961
|
-
for (let i = list.length - 1; i >= 0; i--) {
|
|
962
|
-
const cand = list[i];
|
|
963
|
-
if (inThink(cand))
|
|
964
|
-
continue;
|
|
965
|
-
el = cand;
|
|
966
|
-
break;
|
|
967
|
-
}
|
|
968
|
-
if (el)
|
|
969
|
-
break;
|
|
970
|
-
}
|
|
971
|
-
if (!el)
|
|
972
|
-
return '';
|
|
973
|
-
const out = el.innerText || el.textContent || '';
|
|
974
|
-
const NL = String.fromCharCode(10);
|
|
975
|
-
return out.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim();
|
|
976
|
-
}, { sels: ANSWER_SELECTORS, thinkRe: THINK_CLASS_RE.source });
|
|
870
|
+
return await this.page.evaluate(readLastAnswerInDom, {
|
|
871
|
+
sels: ANSWER_SELECTORS,
|
|
872
|
+
thinkRe: THINK_CLASS_RE.source,
|
|
873
|
+
});
|
|
977
874
|
}
|
|
978
875
|
// We read ONLY visible toasts/notifications, not the whole page text.
|
|
979
876
|
// Otherwise we catch "try again later" from service/hidden blocks and go
|
|
@@ -982,26 +879,7 @@ export class DeepSeekBrowser {
|
|
|
982
879
|
// text from the page and produced a FALSE rate limit (the agent stopped
|
|
983
880
|
// while the chat had no error). Keep only real toast/alert containers.
|
|
984
881
|
async _readPageText() {
|
|
985
|
-
return await this.page
|
|
986
|
-
.evaluate(() => {
|
|
987
|
-
const sels = [
|
|
988
|
-
'[role="alert"]',
|
|
989
|
-
'[class*="toast" i]',
|
|
990
|
-
'[class*="notification" i]',
|
|
991
|
-
];
|
|
992
|
-
let out = '';
|
|
993
|
-
for (const s of sels) {
|
|
994
|
-
for (const el of Array.from(document.querySelectorAll(s))) {
|
|
995
|
-
const e = el;
|
|
996
|
-
const st = getComputedStyle(e);
|
|
997
|
-
if (st.display === 'none' || st.visibility === 'hidden')
|
|
998
|
-
continue;
|
|
999
|
-
out += ' ' + (e.innerText || '');
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
return out;
|
|
1003
|
-
})
|
|
1004
|
-
.catch(() => '');
|
|
882
|
+
return await this.page.evaluate(readPageToastsInDom).catch(() => '');
|
|
1005
883
|
}
|
|
1006
884
|
// The "clean" filter, but read from the DOM only. Used by _askOnce for the
|
|
1007
885
|
// before/after comparison: the network capture must not be compared against
|
|
@@ -1010,16 +888,10 @@ export class DeepSeekBrowser {
|
|
|
1010
888
|
const raw = await this._readLastAnswerTextDom().catch(() => '');
|
|
1011
889
|
return this._cleanAnswer(raw);
|
|
1012
890
|
}
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
1015
|
-
// the tool-call relies on).
|
|
891
|
+
// The pure filter lives in browser-pure.ts (cleanAnswer); kept as a method
|
|
892
|
+
// for the existing `this._cleanAnswer(...)` call sites.
|
|
1016
893
|
_cleanAnswer(raw) {
|
|
1017
|
-
|
|
1018
|
-
if (!t)
|
|
1019
|
-
return '';
|
|
1020
|
-
if (STATUS_RE.test(t))
|
|
1021
|
-
return '';
|
|
1022
|
-
return raw;
|
|
894
|
+
return cleanAnswer(raw);
|
|
1023
895
|
}
|
|
1024
896
|
// Find the Stop button in the DeepSeek UI. We can't rely on the class
|
|
1025
897
|
// alone: during generation the send button (the same circle button)
|
|
@@ -1029,27 +901,7 @@ export class DeepSeekBrowser {
|
|
|
1029
901
|
if (explicit)
|
|
1030
902
|
return true;
|
|
1031
903
|
return await this.page
|
|
1032
|
-
.evaluate(
|
|
1033
|
-
const re = new RegExp(btnRe, 'i');
|
|
1034
|
-
const btns = Array.from(document.querySelectorAll('div[role="button"], button'));
|
|
1035
|
-
for (const b of btns) {
|
|
1036
|
-
const cls = (b.className || '').toString();
|
|
1037
|
-
if (!re.test(cls))
|
|
1038
|
-
continue;
|
|
1039
|
-
const label = ((b.getAttribute('aria-label') || '') +
|
|
1040
|
-
' ' +
|
|
1041
|
-
(b.getAttribute('title') || '') +
|
|
1042
|
-
' ' +
|
|
1043
|
-
(b.textContent || '')).toLowerCase();
|
|
1044
|
-
if (/stop|останов/.test(label))
|
|
1045
|
-
return true;
|
|
1046
|
-
// A square icon = Stop button; an arrow (path without rect) = send.
|
|
1047
|
-
const svg = b.querySelector('svg');
|
|
1048
|
-
if (svg && svg.querySelector('rect'))
|
|
1049
|
-
return true;
|
|
1050
|
-
}
|
|
1051
|
-
return false;
|
|
1052
|
-
}, BUTTON_CLASS_RE.source)
|
|
904
|
+
.evaluate(stopButtonVisibleInDom, BUTTON_CLASS_RE.source)
|
|
1053
905
|
.catch(() => false);
|
|
1054
906
|
}
|
|
1055
907
|
async _isGenerating() {
|
|
@@ -1063,16 +915,10 @@ export class DeepSeekBrowser {
|
|
|
1063
915
|
// all we need to tell "the page grew / a new answer is streaming".
|
|
1064
916
|
async _chatSignal() {
|
|
1065
917
|
return await this.page
|
|
1066
|
-
.evaluate(
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
if (answers.length) {
|
|
1071
|
-
const lastEl = answers[answers.length - 1];
|
|
1072
|
-
lastLen = (lastEl.innerText || '').length;
|
|
1073
|
-
}
|
|
1074
|
-
return { nodes, lastLen };
|
|
1075
|
-
}, { nodesSel: CHAT_SIGNAL_NODES_SELECTOR, mdSel: MARKDOWN_SELECTOR })
|
|
918
|
+
.evaluate(chatSignalInDom, {
|
|
919
|
+
nodesSel: CHAT_SIGNAL_NODES_SELECTOR,
|
|
920
|
+
mdSel: MARKDOWN_SELECTOR,
|
|
921
|
+
})
|
|
1076
922
|
.catch(() => ({ nodes: 0, lastLen: 0 }));
|
|
1077
923
|
}
|
|
1078
924
|
// Cheap first probe for the Continue button. Runs ONE page.evaluate
|
|
@@ -1083,26 +929,7 @@ export class DeepSeekBrowser {
|
|
|
1083
929
|
// HITS do we run the full scan to get a clickable Locator.
|
|
1084
930
|
async _continueVisibleCheap() {
|
|
1085
931
|
return await this.page
|
|
1086
|
-
.evaluate(
|
|
1087
|
-
const re = new RegExp(nameRe, 'i');
|
|
1088
|
-
const cands = Array.from(document.querySelectorAll('div[role="button"], button'));
|
|
1089
|
-
for (const e of cands) {
|
|
1090
|
-
const label = ((e.textContent || '') +
|
|
1091
|
-
' ' +
|
|
1092
|
-
(e.getAttribute('aria-label') || ''))
|
|
1093
|
-
.replace(/\s+/g, ' ')
|
|
1094
|
-
.trim();
|
|
1095
|
-
if (!re.test(label))
|
|
1096
|
-
continue;
|
|
1097
|
-
const st = getComputedStyle(e);
|
|
1098
|
-
if (st.display === 'none' || st.visibility === 'hidden')
|
|
1099
|
-
continue;
|
|
1100
|
-
const r = e.getBoundingClientRect();
|
|
1101
|
-
if (r.width > 0 && r.height > 0)
|
|
1102
|
-
return true;
|
|
1103
|
-
}
|
|
1104
|
-
return false;
|
|
1105
|
-
}, CONTINUE_NAME_RE.source)
|
|
932
|
+
.evaluate(continueVisibleCheapInDom, CONTINUE_NAME_RE.source)
|
|
1106
933
|
.catch(() => false);
|
|
1107
934
|
}
|
|
1108
935
|
// Is DeepSeek's "Continue" button currently visible? Used to BLOCK the
|
|
@@ -1128,26 +955,7 @@ export class DeepSeekBrowser {
|
|
|
1128
955
|
catch { }
|
|
1129
956
|
// Fallback: the raw DOM scan (older/newer DeepSeek builds where the role
|
|
1130
957
|
// is absent).
|
|
1131
|
-
return this.page
|
|
1132
|
-
.evaluate(() => {
|
|
1133
|
-
const labelOf = (b) => ((b.textContent || '') + ' ' + (b.getAttribute('aria-label') || ''))
|
|
1134
|
-
.replace(/\s+/g, ' ')
|
|
1135
|
-
.trim();
|
|
1136
|
-
const isExact = (t) => /^(?:continue|продолжить|продолжение)(?:\s+(?:think(?:ing)?|reason(?:ing)?|размышлени[ея]|генераци[юя]|ответ))?\s*[.!…]?$/i.test(t);
|
|
1137
|
-
const cands = Array.from(document.querySelectorAll('div[role="button"], button'));
|
|
1138
|
-
for (const e of cands) {
|
|
1139
|
-
if (!isExact(labelOf(e)))
|
|
1140
|
-
continue;
|
|
1141
|
-
const st = getComputedStyle(e);
|
|
1142
|
-
if (st.display === 'none' || st.visibility === 'hidden')
|
|
1143
|
-
continue;
|
|
1144
|
-
const r = e.getBoundingClientRect();
|
|
1145
|
-
if (r.width > 0 && r.height > 0)
|
|
1146
|
-
return true;
|
|
1147
|
-
}
|
|
1148
|
-
return false;
|
|
1149
|
-
})
|
|
1150
|
-
.catch(() => false);
|
|
958
|
+
return this.page.evaluate(continueVisibleInDom).catch(() => false);
|
|
1151
959
|
}
|
|
1152
960
|
// Click the DeepSeek "Continue" button when it is visible (reasoning/answer
|
|
1153
961
|
// paused by the server). Returns true when a click happened.
|
|
@@ -1227,44 +1035,7 @@ export class DeepSeekBrowser {
|
|
|
1227
1035
|
// 2) Fallback: raw DOM scan + full pointer/mouse event sequence (an older
|
|
1228
1036
|
// build without a proper role, or a click intercepted by an overlay).
|
|
1229
1037
|
try {
|
|
1230
|
-
const clicked = await this.page.evaluate(
|
|
1231
|
-
const labelOf = (b) => ((b.textContent || '') + ' ' + (b.getAttribute('aria-label') || ''))
|
|
1232
|
-
.replace(/\s+/g, ' ')
|
|
1233
|
-
.trim();
|
|
1234
|
-
const isExact = (t) => /^(?:continue|продолжить|продолжение)(?:\s+(?:think(?:ing)?|reason(?:ing)?|размышлени[ея]|генераци[юя]|ответ))?\s*[.!…]?$/i.test(t);
|
|
1235
|
-
const cands = Array.from(document.querySelectorAll('div[role="button"], button'));
|
|
1236
|
-
for (const e of cands) {
|
|
1237
|
-
const t = labelOf(e);
|
|
1238
|
-
if (!isExact(t))
|
|
1239
|
-
continue;
|
|
1240
|
-
const st = getComputedStyle(e);
|
|
1241
|
-
if (st.display === 'none' || st.visibility === 'hidden')
|
|
1242
|
-
continue;
|
|
1243
|
-
const rect = e.getBoundingClientRect();
|
|
1244
|
-
if (rect.width === 0 || rect.height === 0)
|
|
1245
|
-
continue;
|
|
1246
|
-
const cx = rect.left + rect.width / 2;
|
|
1247
|
-
const cy = rect.top + rect.height / 2;
|
|
1248
|
-
const opts = {
|
|
1249
|
-
bubbles: true,
|
|
1250
|
-
cancelable: true,
|
|
1251
|
-
composed: true,
|
|
1252
|
-
clientX: cx,
|
|
1253
|
-
clientY: cy,
|
|
1254
|
-
button: 0,
|
|
1255
|
-
};
|
|
1256
|
-
try {
|
|
1257
|
-
e.dispatchEvent(new PointerEvent('pointerdown', opts));
|
|
1258
|
-
e.dispatchEvent(new MouseEvent('mousedown', opts));
|
|
1259
|
-
e.dispatchEvent(new PointerEvent('pointerup', opts));
|
|
1260
|
-
e.dispatchEvent(new MouseEvent('mouseup', opts));
|
|
1261
|
-
}
|
|
1262
|
-
catch { }
|
|
1263
|
-
e.click();
|
|
1264
|
-
return true;
|
|
1265
|
-
}
|
|
1266
|
-
return false;
|
|
1267
|
-
});
|
|
1038
|
+
const clicked = await this.page.evaluate(clickContinueInDom);
|
|
1268
1039
|
if (clicked) {
|
|
1269
1040
|
this._lastContinueAt = Date.now();
|
|
1270
1041
|
this._notice(this._t('ds.continue_clicked'));
|
|
@@ -1385,30 +1156,7 @@ export class DeepSeekBrowser {
|
|
|
1385
1156
|
// 3) Raw DOM scan + the full pointer/mouse sequence (older builds without
|
|
1386
1157
|
// a proper role, or a click intercepted by an overlay).
|
|
1387
1158
|
const clicked = await this.page
|
|
1388
|
-
.evaluate(
|
|
1389
|
-
const re = new RegExp(btnRe, 'i');
|
|
1390
|
-
const btns = Array.from(document.querySelectorAll('div[role="button"], button'));
|
|
1391
|
-
for (const b of btns) {
|
|
1392
|
-
const cls = (b.className || '').toString();
|
|
1393
|
-
if (!re.test(cls))
|
|
1394
|
-
continue;
|
|
1395
|
-
const svg = b.querySelector('svg');
|
|
1396
|
-
if (svg && svg.querySelector('rect')) {
|
|
1397
|
-
for (const type of [
|
|
1398
|
-
'pointerdown',
|
|
1399
|
-
'mousedown',
|
|
1400
|
-
'pointerup',
|
|
1401
|
-
'mouseup',
|
|
1402
|
-
'click',
|
|
1403
|
-
]) {
|
|
1404
|
-
b.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true }));
|
|
1405
|
-
}
|
|
1406
|
-
b.click();
|
|
1407
|
-
return true;
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
return false;
|
|
1411
|
-
}, BUTTON_CLASS_RE.source)
|
|
1159
|
+
.evaluate(clickStopInDom, BUTTON_CLASS_RE.source)
|
|
1412
1160
|
.catch(() => false);
|
|
1413
1161
|
if (clicked)
|
|
1414
1162
|
return true;
|
|
@@ -1578,7 +1326,7 @@ export class DeepSeekBrowser {
|
|
|
1578
1326
|
this._askCancelled = true;
|
|
1579
1327
|
}
|
|
1580
1328
|
async _setInputText(input, text) {
|
|
1581
|
-
const tag = await input.evaluate(
|
|
1329
|
+
const tag = await input.evaluate(elementTagNameInDom);
|
|
1582
1330
|
const isNative = tag === 'textarea' || tag === 'input';
|
|
1583
1331
|
if (isNative) {
|
|
1584
1332
|
try {
|
|
@@ -1590,28 +1338,11 @@ export class DeepSeekBrowser {
|
|
|
1590
1338
|
await input.click();
|
|
1591
1339
|
await this.page.keyboard.press('Control+A');
|
|
1592
1340
|
await this.page.keyboard.press('Delete');
|
|
1593
|
-
const ok = await input.evaluate(
|
|
1594
|
-
el.focus();
|
|
1595
|
-
const dt = new DataTransfer();
|
|
1596
|
-
dt.setData('text/plain', value);
|
|
1597
|
-
const ev = new ClipboardEvent('paste', {
|
|
1598
|
-
clipboardData: dt,
|
|
1599
|
-
bubbles: true,
|
|
1600
|
-
cancelable: true,
|
|
1601
|
-
});
|
|
1602
|
-
el.dispatchEvent(ev);
|
|
1603
|
-
return true;
|
|
1604
|
-
}, text);
|
|
1341
|
+
const ok = await input.evaluate(pasteTextIntoElement, text);
|
|
1605
1342
|
if (!ok) {
|
|
1606
1343
|
await this.page.keyboard.insertText(text);
|
|
1607
1344
|
}
|
|
1608
|
-
const got = await input.evaluate(
|
|
1609
|
-
if (el.tagName.toLowerCase() === 'textarea' ||
|
|
1610
|
-
el.tagName.toLowerCase() === 'input') {
|
|
1611
|
-
return el.value;
|
|
1612
|
-
}
|
|
1613
|
-
return el.innerText || el.textContent || '';
|
|
1614
|
-
});
|
|
1345
|
+
const got = await input.evaluate(readElementText);
|
|
1615
1346
|
const norm = (s) => (s || '')
|
|
1616
1347
|
.replace(/\r\n/g, '\n')
|
|
1617
1348
|
.replace(/\u00a0/g, ' ')
|
|
@@ -1625,12 +1356,7 @@ export class DeepSeekBrowser {
|
|
|
1625
1356
|
await this.page.keyboard.press('Control+A');
|
|
1626
1357
|
await this.page.keyboard.press('Delete');
|
|
1627
1358
|
await this.page.keyboard.insertText(text);
|
|
1628
|
-
const got2 = await input.evaluate(
|
|
1629
|
-
if (el.tagName.toLowerCase() === 'textarea' ||
|
|
1630
|
-
el.tagName.toLowerCase() === 'input')
|
|
1631
|
-
return el.value;
|
|
1632
|
-
return el.innerText || el.textContent || '';
|
|
1633
|
-
});
|
|
1359
|
+
const got2 = await input.evaluate(readElementText);
|
|
1634
1360
|
if (norm(got2) !== norm(text)) {
|
|
1635
1361
|
throw new Error(this._t('ds.input_partial', {
|
|
1636
1362
|
got: norm(got2).length,
|
|
@@ -1920,14 +1646,7 @@ export class DeepSeekBrowser {
|
|
|
1920
1646
|
// attribute (and a disabled class) directly — during an upload the
|
|
1921
1647
|
// button is aria-disabled=true and a click is silently ignored.
|
|
1922
1648
|
const enabled = await btn
|
|
1923
|
-
.evaluate(
|
|
1924
|
-
if (el.getAttribute('aria-disabled') === 'true')
|
|
1925
|
-
return false;
|
|
1926
|
-
const cls = (el.className || '').toString();
|
|
1927
|
-
if (/disabled|is-disabled/.test(cls))
|
|
1928
|
-
return false;
|
|
1929
|
-
return true;
|
|
1930
|
-
})
|
|
1649
|
+
.evaluate(isElementEnabledInDom)
|
|
1931
1650
|
.catch(() => true);
|
|
1932
1651
|
if (!enabled)
|
|
1933
1652
|
continue;
|
|
@@ -1953,7 +1672,7 @@ export class DeepSeekBrowser {
|
|
|
1953
1672
|
try {
|
|
1954
1673
|
await this.page.waitForTimeout(300);
|
|
1955
1674
|
const stillThere = await input
|
|
1956
|
-
.evaluate(
|
|
1675
|
+
.evaluate(readInputTrimmedTextInDom)
|
|
1957
1676
|
.catch(() => '');
|
|
1958
1677
|
if (stillThere) {
|
|
1959
1678
|
this._askDebug('SEND not cleared, pressing Enter again');
|
|
@@ -2333,19 +2052,7 @@ export class DeepSeekBrowser {
|
|
|
2333
2052
|
throw new Error(this._t('ds.not_launched'));
|
|
2334
2053
|
const html = await this.page.content();
|
|
2335
2054
|
await fs.writeFile(filePath, html, 'utf-8');
|
|
2336
|
-
const report = await this.page.evaluate(
|
|
2337
|
-
const result = { answers: {}, stops: {}, inputs: {} };
|
|
2338
|
-
for (const s of sels.answers) {
|
|
2339
|
-
result.answers[s] = document.querySelectorAll(s).length;
|
|
2340
|
-
}
|
|
2341
|
-
for (const s of sels.stops) {
|
|
2342
|
-
result.stops[s] = document.querySelectorAll(s).length;
|
|
2343
|
-
}
|
|
2344
|
-
for (const s of sels.inputs) {
|
|
2345
|
-
result.inputs[s] = document.querySelectorAll(s).length;
|
|
2346
|
-
}
|
|
2347
|
-
return result;
|
|
2348
|
-
}, {
|
|
2055
|
+
const report = await this.page.evaluate(dumpDomCountsInDom, {
|
|
2349
2056
|
answers: ANSWER_SELECTORS,
|
|
2350
2057
|
stops: STOP_SELECTORS,
|
|
2351
2058
|
inputs: INPUT_SELECTORS,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.72.
|
|
3
|
+
"version": "2.72.1",
|
|
4
4
|
"description": "Terminal coding agent that drives chat.deepseek.com through Playwright — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|