zames_pro 2.0.0 → 2.1.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/dist/agent-loop.js +30 -4
- package/dist/browser.js +120 -106
- package/dist/config.js +0 -1
- package/dist/confirm.js +0 -1
- package/dist/diff.js +0 -1
- package/dist/gitTools.js +0 -1
- package/dist/index.js +88 -56
- package/dist/input.js +0 -1
- package/dist/markdown.js +0 -1
- package/dist/net-capture.js +145 -0
- package/dist/self-review.js +0 -1
- package/dist/sessions.js +0 -1
- package/dist/spinner.js +0 -1
- package/dist/system-prompt.js +14 -1
- package/dist/theme.js +0 -1
- package/dist/tools.js +36 -14
- package/dist/transcript.js +0 -1
- package/dist/types.js +0 -1
- package/dist/undo.js +0 -1
- package/dist/web.js +0 -1
- package/dist/xml-toolcall.js +0 -1
- package/package.json +7 -3
- package/dist/agent-loop.js.map +0 -1
- package/dist/browser.js.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/confirm.js.map +0 -1
- package/dist/diff.js.map +0 -1
- package/dist/gitTools.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/input.js.map +0 -1
- package/dist/markdown.js.map +0 -1
- package/dist/self-review.js.map +0 -1
- package/dist/sessions.js.map +0 -1
- package/dist/spinner.js.map +0 -1
- package/dist/system-prompt.js.map +0 -1
- package/dist/theme.js.map +0 -1
- package/dist/tools.js.map +0 -1
- package/dist/transcript.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/undo.js.map +0 -1
- package/dist/web.js.map +0 -1
- package/dist/xml-toolcall.js.map +0 -1
package/dist/agent-loop.js
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import { buildSystemPrompt } from './system-prompt.js';
|
|
2
2
|
import { getGitContext, formatGitContext } from './gitTools.js';
|
|
3
3
|
import { parseXmlToolCalls } from './xml-toolcall.js';
|
|
4
|
-
export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 40, freshChat = false, sendSystemPrompt = false, transcript = null, onThinking = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, debugLog = false, }) {
|
|
4
|
+
export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 40, freshChat = false, sendSystemPrompt = false, transcript = null, onThinking = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, debugLog = false, }) {
|
|
5
5
|
if (freshChat) {
|
|
6
6
|
await browser.newChat();
|
|
7
7
|
transcript?.log('new_chat');
|
|
8
8
|
}
|
|
9
|
+
// Сообщаем вызывающему актуальный chat id. После newChat() URL ещё без id
|
|
10
|
+
// (он появляется только после первой отправки), поэтому зовём колбэк и
|
|
11
|
+
// здесь, и после первой реальной отправки ниже.
|
|
12
|
+
let lastReportedChatId = null;
|
|
13
|
+
const reportChat = async () => {
|
|
14
|
+
let id = null;
|
|
15
|
+
try {
|
|
16
|
+
id = await browser.getCurrentChatId();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
id = null;
|
|
20
|
+
}
|
|
21
|
+
if (id && id !== lastReportedChatId) {
|
|
22
|
+
lastReportedChatId = id;
|
|
23
|
+
onChatReady(id);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
await reportChat();
|
|
9
27
|
if (sendSystemPrompt) {
|
|
10
28
|
let gitText = null;
|
|
11
29
|
try {
|
|
@@ -26,6 +44,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
26
44
|
});
|
|
27
45
|
onThinking();
|
|
28
46
|
await browser.ask(systemPrompt, { timeout: 60_000 });
|
|
47
|
+
await reportChat();
|
|
29
48
|
}
|
|
30
49
|
let message = task;
|
|
31
50
|
transcript?.log('task', { task });
|
|
@@ -37,6 +56,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
37
56
|
for (let i = 0; i < maxIterations; i++) {
|
|
38
57
|
onThinking();
|
|
39
58
|
const rawResponse = await browser.ask(message);
|
|
59
|
+
await reportChat();
|
|
40
60
|
transcript?.log('assistant_raw', { response: rawResponse });
|
|
41
61
|
const parsed = parseToolCall(rawResponse);
|
|
42
62
|
if (parsed) {
|
|
@@ -60,9 +80,16 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
60
80
|
const looksLikeToolCall = /("tool"\s*:|\btool_calls?\b|\binvoke\b|\bparameter\b|DSML|function_call)/i.test(rawResponse) || /\{\s*"?(tool|name)"?\s*:/.test(rawResponse);
|
|
61
81
|
if (looksLikeToolCall && malformedRetries < MAX_MALFORMED_RETRIES) {
|
|
62
82
|
malformedRetries++;
|
|
63
|
-
transcript?.log(
|
|
83
|
+
transcript?.log('malformed_toolcall', {
|
|
84
|
+
attempt: malformedRetries,
|
|
85
|
+
response: rawResponse,
|
|
86
|
+
});
|
|
64
87
|
if (debugLog) {
|
|
65
|
-
console.error(
|
|
88
|
+
console.error('внимание: ответ похож на tool-call, но не распознан (попытка ' +
|
|
89
|
+
malformedRetries +
|
|
90
|
+
'/' +
|
|
91
|
+
MAX_MALFORMED_RETRIES +
|
|
92
|
+
')');
|
|
66
93
|
}
|
|
67
94
|
message =
|
|
68
95
|
'Твой предыдущий ответ не распознан как вызов инструмента. ' +
|
|
@@ -649,4 +676,3 @@ function extractPreToolText(text) {
|
|
|
649
676
|
return '';
|
|
650
677
|
return text.slice(0, earliest).trim();
|
|
651
678
|
}
|
|
652
|
-
//# sourceMappingURL=agent-loop.js.map
|
package/dist/browser.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { chromium } from 'playwright';
|
|
1
|
+
import { chromium, } from 'playwright';
|
|
2
|
+
import { extractAnswer, dumpNetBody } from './net-capture.js';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import os from 'os';
|
|
4
5
|
import fs from 'fs/promises';
|
|
@@ -12,10 +13,25 @@ const INPUT_SELECTORS = [
|
|
|
12
13
|
'[role="textbox"]',
|
|
13
14
|
];
|
|
14
15
|
const ANSWER_SELECTORS = [
|
|
16
|
+
'div.ds-assistant-message-main-content',
|
|
17
|
+
'div[class*="ds-assistant-message-main-content"]',
|
|
15
18
|
'div[class*="ds-markdown"]',
|
|
16
19
|
'div[class*="markdown"]',
|
|
17
20
|
];
|
|
21
|
+
const SEND_SELECTORS = [
|
|
22
|
+
'div[role="button"].ds-button--primary.ds-button--circle',
|
|
23
|
+
'div[role="button"].ds-button--primary.ds-button--filled',
|
|
24
|
+
'button[type="submit"]',
|
|
25
|
+
'button[aria-label*="send" i]',
|
|
26
|
+
'button[aria-label*="отправ" i]',
|
|
27
|
+
];
|
|
28
|
+
// ВАЖНО: сюда НЕЛЬЗЯ добавлять общий 'div[role="button"][class*="ds-button--primary"]'
|
|
29
|
+
// — под него попадает кнопка отправки, которая видна всегда, и тогда
|
|
30
|
+
// _isGenerating() вечно возвращает true, из-за чего ответ никогда не
|
|
31
|
+
// считается готовым.
|
|
18
32
|
const STOP_SELECTORS = [
|
|
33
|
+
'div[role="button"][aria-label*="stop" i]',
|
|
34
|
+
'div[role="button"][aria-label*="останов" i]',
|
|
19
35
|
'button:has-text("Stop")',
|
|
20
36
|
'button:has-text("Остановить")',
|
|
21
37
|
'button[aria-label*="Stop" i]',
|
|
@@ -71,6 +87,12 @@ export class DeepSeekBrowser {
|
|
|
71
87
|
_abort;
|
|
72
88
|
context;
|
|
73
89
|
page;
|
|
90
|
+
_netCapture;
|
|
91
|
+
_netCaptureAt;
|
|
92
|
+
_netChatId;
|
|
93
|
+
_netSniff;
|
|
94
|
+
_netSniffLimit;
|
|
95
|
+
_netHookInstalled;
|
|
74
96
|
constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 3, stabilityDelayMs = 1000, minSendIntervalMs = 15000, } = {}) {
|
|
75
97
|
this.headless = headless;
|
|
76
98
|
this.debug = debug;
|
|
@@ -79,11 +101,15 @@ export class DeepSeekBrowser {
|
|
|
79
101
|
this.askRetries = askRetries;
|
|
80
102
|
this.stabilityChecks = stabilityChecks;
|
|
81
103
|
this.stabilityDelayMs = stabilityDelayMs;
|
|
82
|
-
// Минимальный интервал между отправками в чат: DeepSeek ограничивает
|
|
83
|
-
// частоту («Messages too frequent. Try again later.»).
|
|
84
104
|
this.minSendIntervalMs = minSendIntervalMs;
|
|
85
105
|
this._lastSentAt = 0;
|
|
86
106
|
this._abort = false;
|
|
107
|
+
this._netCapture = '';
|
|
108
|
+
this._netCaptureAt = 0;
|
|
109
|
+
this._netChatId = null;
|
|
110
|
+
this._netSniff = [];
|
|
111
|
+
this._netSniffLimit = 5;
|
|
112
|
+
this._netHookInstalled = false;
|
|
87
113
|
}
|
|
88
114
|
async launch() {
|
|
89
115
|
await killStaleChrome();
|
|
@@ -126,8 +152,51 @@ export class DeepSeekBrowser {
|
|
|
126
152
|
}
|
|
127
153
|
}
|
|
128
154
|
this.page = this.context.pages()[0] || (await this.context.newPage());
|
|
155
|
+
this._installNetHook();
|
|
129
156
|
await this.page.goto(CHAT_URL, { waitUntil: 'domcontentloaded' });
|
|
130
157
|
}
|
|
158
|
+
_installNetHook() {
|
|
159
|
+
if (this._netHookInstalled)
|
|
160
|
+
return;
|
|
161
|
+
const pg = this['page'];
|
|
162
|
+
if (!pg)
|
|
163
|
+
return;
|
|
164
|
+
this._netHookInstalled = true;
|
|
165
|
+
pg.on('response', (resp) => {
|
|
166
|
+
void this._onResponse(resp).catch(() => { });
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async _onResponse(resp) {
|
|
170
|
+
try {
|
|
171
|
+
const url = resp.url();
|
|
172
|
+
if (!/deepseek\.com/i.test(url))
|
|
173
|
+
return;
|
|
174
|
+
if (/\.(js|css|png|jpg|jpeg|svg|woff2?|ico|map)(\?|$)/i.test(url))
|
|
175
|
+
return;
|
|
176
|
+
const ct = (resp.headers()['content-type'] || '').toLowerCase();
|
|
177
|
+
const interesting = ct.includes('event-stream') ||
|
|
178
|
+
ct.includes('json') ||
|
|
179
|
+
ct.includes('text/plain');
|
|
180
|
+
if (!interesting)
|
|
181
|
+
return;
|
|
182
|
+
const body = await resp.text().catch(() => '');
|
|
183
|
+
if (!body)
|
|
184
|
+
return;
|
|
185
|
+
this._netSniff.push({ url, contentType: ct, body });
|
|
186
|
+
if (this._netSniff.length > this._netSniffLimit)
|
|
187
|
+
this._netSniff.shift();
|
|
188
|
+
void dumpNetBody(url, body);
|
|
189
|
+
const extracted = extractAnswer(body);
|
|
190
|
+
if (extracted) {
|
|
191
|
+
this._netCapture = extracted;
|
|
192
|
+
const cid = url.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
|
|
193
|
+
if (cid)
|
|
194
|
+
this._netChatId = cid[0];
|
|
195
|
+
this._netCaptureAt = Date.now();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch { }
|
|
199
|
+
}
|
|
131
200
|
async restart() {
|
|
132
201
|
try {
|
|
133
202
|
if (this.context)
|
|
@@ -193,6 +262,12 @@ export class DeepSeekBrowser {
|
|
|
193
262
|
return null;
|
|
194
263
|
}
|
|
195
264
|
async _readLastAnswerText() {
|
|
265
|
+
// Если удалось перехватить сырой текст ответа по сети (без рендер-
|
|
266
|
+
// искажений DeepSeek) и он относится к текущему ответу — отдаём его.
|
|
267
|
+
// Это защищает $, экранированные переводы строк и т.п. в аргументах.
|
|
268
|
+
if (this._netCapture && this._netCaptureAt >= this._lastSentAt) {
|
|
269
|
+
return this._netCapture;
|
|
270
|
+
}
|
|
196
271
|
return await this.page.evaluate((sels) => {
|
|
197
272
|
let el = null;
|
|
198
273
|
for (const s of sels) {
|
|
@@ -202,74 +277,8 @@ export class DeepSeekBrowser {
|
|
|
202
277
|
}
|
|
203
278
|
if (!el)
|
|
204
279
|
return '';
|
|
205
|
-
const
|
|
206
|
-
Array.from(clone.querySelectorAll("pre")).forEach((pre) => {
|
|
207
|
-
const codeEl = pre.querySelector('code');
|
|
208
|
-
const source = codeEl || pre;
|
|
209
|
-
const text = (source.textContent || '').replace(/\n$/, '');
|
|
210
|
-
const langMatch = (source.className || '').match(/language-([\w-]+)/);
|
|
211
|
-
const lang = langMatch ? langMatch[1] : '';
|
|
212
|
-
const replacement = document.createTextNode('```' + lang + '\n' + text + '\n```');
|
|
213
|
-
if (pre.parentNode)
|
|
214
|
-
pre.parentNode.replaceChild(replacement, pre);
|
|
215
|
-
});
|
|
216
|
-
Array.from(clone.querySelectorAll("code")).forEach((c) => {
|
|
217
|
-
const replacement = document.createTextNode('`' + (c.textContent || '') + '`');
|
|
218
|
-
if (c.parentNode)
|
|
219
|
-
c.parentNode.replaceChild(replacement, c);
|
|
220
|
-
});
|
|
221
|
-
// textContent склеивает блоки без переводов строк, из-за чего
|
|
222
|
-
// Markdown-рендер получает одну длинную строку. Обходим DOM сами и
|
|
223
|
-
// расставляем переводы строк / маркеры Markdown по блочным элементам.
|
|
280
|
+
const out = el.innerText || el.textContent || '';
|
|
224
281
|
const NL = String.fromCharCode(10);
|
|
225
|
-
const BULLET = String.fromCharCode(45) + ' '; // '- '
|
|
226
|
-
function domToMarkdown(node) {
|
|
227
|
-
if (node.nodeType === 3)
|
|
228
|
-
return node.textContent || '';
|
|
229
|
-
if (node.nodeType !== 1)
|
|
230
|
-
return '';
|
|
231
|
-
const tag = node.tagName.toLowerCase();
|
|
232
|
-
if (tag === 'br')
|
|
233
|
-
return NL;
|
|
234
|
-
const inner = Array.from(node.childNodes)
|
|
235
|
-
.map(domToMarkdown)
|
|
236
|
-
.join('');
|
|
237
|
-
const STAR = String.fromCharCode(42); // '*'
|
|
238
|
-
if (tag === 'strong' || tag === 'b')
|
|
239
|
-
return STAR + STAR + inner + STAR + STAR;
|
|
240
|
-
if (tag === 'em' || tag === 'i')
|
|
241
|
-
return STAR + inner + STAR;
|
|
242
|
-
if (tag === 'del' || tag === 's')
|
|
243
|
-
return '~~' + inner + '~~';
|
|
244
|
-
if (tag === 'a') {
|
|
245
|
-
const href = node.getAttribute('href') || '';
|
|
246
|
-
return href ? '[' + inner + '](' + href + ')' : inner;
|
|
247
|
-
}
|
|
248
|
-
if (/^h[1-6]$/.test(tag)) {
|
|
249
|
-
const level = Number(tag[1]);
|
|
250
|
-
const hashes = '#'.repeat(level);
|
|
251
|
-
return NL + NL + hashes + ' ' + inner.trim() + NL + NL;
|
|
252
|
-
}
|
|
253
|
-
if (tag === 'li') {
|
|
254
|
-
const text = inner.trim().replace(new RegExp(NL + '+', 'g'), ' ');
|
|
255
|
-
return BULLET + text + NL;
|
|
256
|
-
}
|
|
257
|
-
if (tag === 'ul' || tag === 'ol' || tag === 'blockquote') {
|
|
258
|
-
return NL + inner + NL;
|
|
259
|
-
}
|
|
260
|
-
if (tag === 'p' ||
|
|
261
|
-
tag === 'div' ||
|
|
262
|
-
tag === 'section' ||
|
|
263
|
-
tag === 'article' ||
|
|
264
|
-
tag === 'tr' ||
|
|
265
|
-
tag === 'table') {
|
|
266
|
-
const text = inner.trim();
|
|
267
|
-
return text ? NL + NL + text : '';
|
|
268
|
-
}
|
|
269
|
-
return inner;
|
|
270
|
-
}
|
|
271
|
-
const out = domToMarkdown(clone);
|
|
272
|
-
// Схлопываем тройные+ переводы строк до двойных (разделитель блоков).
|
|
273
282
|
return out.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim();
|
|
274
283
|
}, ANSWER_SELECTORS);
|
|
275
284
|
}
|
|
@@ -278,7 +287,6 @@ export class DeepSeekBrowser {
|
|
|
278
287
|
const t = (raw || '').trim();
|
|
279
288
|
if (!t)
|
|
280
289
|
return '';
|
|
281
|
-
// Отсекаем служебные статусы интерфейса (Reading..., Думаю...).
|
|
282
290
|
if (STATUS_RE.test(t))
|
|
283
291
|
return '';
|
|
284
292
|
return raw;
|
|
@@ -287,8 +295,6 @@ export class DeepSeekBrowser {
|
|
|
287
295
|
const stop = await this._findVisible(STOP_SELECTORS, 300);
|
|
288
296
|
return !!stop;
|
|
289
297
|
}
|
|
290
|
-
// Прервать текущую генерацию: нажать Stop в интерфейсе.
|
|
291
|
-
// Используется при нажатии Esc пользователем.
|
|
292
298
|
async stopGeneration() {
|
|
293
299
|
this._abort = true;
|
|
294
300
|
const btn = await this._findVisible(STOP_SELECTORS, 500);
|
|
@@ -327,14 +333,6 @@ export class DeepSeekBrowser {
|
|
|
327
333
|
}
|
|
328
334
|
throw new Error(`ask() провалился после ${this.askRetries} попыток: ${lastErr?.message}`);
|
|
329
335
|
}
|
|
330
|
-
// Вставка текста в поле ввода.
|
|
331
|
-
//
|
|
332
|
-
// fill()/insertText() ломаются на многострочном тексте в contenteditable-
|
|
333
|
-
// редакторах (ProseMirror/Lexical/...): символ перевода строки там
|
|
334
|
-
// трактуется как Enter, и в поле остаётся только первая строка. Поэтому
|
|
335
|
-
// для contenteditable и [role=textbox] эмулируем paste-событие с полным
|
|
336
|
-
// текстом — то же, что делает Shift+Insert. Для нативных textarea/input
|
|
337
|
-
// перевод строки работает и так.
|
|
338
336
|
async _setInputText(input, text) {
|
|
339
337
|
const tag = await input.evaluate((el) => el.tagName.toLowerCase());
|
|
340
338
|
const isNative = tag === 'textarea' || tag === 'input';
|
|
@@ -346,7 +344,6 @@ export class DeepSeekBrowser {
|
|
|
346
344
|
catch { }
|
|
347
345
|
}
|
|
348
346
|
await input.click();
|
|
349
|
-
// Выделяем всё содержимое, чтобы вставка заменила его целиком.
|
|
350
347
|
await this.page.keyboard.press('Control+A');
|
|
351
348
|
await this.page.keyboard.press('Delete');
|
|
352
349
|
const ok = await input.evaluate((el, value) => {
|
|
@@ -359,32 +356,30 @@ export class DeepSeekBrowser {
|
|
|
359
356
|
cancelable: true,
|
|
360
357
|
});
|
|
361
358
|
el.dispatchEvent(ev);
|
|
362
|
-
// Если обработчик не отменил вставку и поле осталось пустым —
|
|
363
|
-
// пробуем через insertText вручную (fallback ниже).
|
|
364
359
|
return true;
|
|
365
360
|
}, text);
|
|
366
361
|
if (!ok) {
|
|
367
362
|
await this.page.keyboard.insertText(text);
|
|
368
363
|
}
|
|
369
|
-
// Проверяем, что текст реально попал в поле. Если редактор проигнорировал
|
|
370
|
-
// paste — падаем на insertText.
|
|
371
364
|
const got = await input.evaluate((el) => {
|
|
372
|
-
if (el.tagName.toLowerCase() === 'textarea' ||
|
|
365
|
+
if (el.tagName.toLowerCase() === 'textarea' ||
|
|
366
|
+
el.tagName.toLowerCase() === 'input') {
|
|
373
367
|
return el.value;
|
|
374
368
|
}
|
|
375
369
|
return el.innerText || el.textContent || '';
|
|
376
370
|
});
|
|
377
|
-
const norm = (s) => (s || '')
|
|
371
|
+
const norm = (s) => (s || '')
|
|
372
|
+
.replace(/\r\n/g, '\n')
|
|
373
|
+
.replace(/\u00a0/g, ' ')
|
|
374
|
+
.trim();
|
|
378
375
|
if (!norm(got)) {
|
|
379
376
|
await input.click();
|
|
380
377
|
await this.page.keyboard.insertText(text);
|
|
381
378
|
}
|
|
382
379
|
}
|
|
383
|
-
// Ждём, пока с прошлой отправки пройдёт minSendIntervalMs. Защита от
|
|
384
|
-
// «Messages too frequent. Try again later.» на [chat.deepseek.com](https://chat.deepseek.com/).
|
|
385
380
|
async _waitForSendSlot() {
|
|
386
381
|
if (!this._lastSentAt)
|
|
387
|
-
return;
|
|
382
|
+
return;
|
|
388
383
|
const gap = this.minSendIntervalMs - (Date.now() - this._lastSentAt);
|
|
389
384
|
if (gap <= 0)
|
|
390
385
|
return;
|
|
@@ -398,25 +393,42 @@ export class DeepSeekBrowser {
|
|
|
398
393
|
}
|
|
399
394
|
const beforeText = await this._readLastAnswerTextClean().catch(() => '');
|
|
400
395
|
await this._waitForSendSlot();
|
|
396
|
+
this._netCapture = '';
|
|
397
|
+
this._netCaptureAt = 0;
|
|
401
398
|
await this._setInputText(input, prompt);
|
|
402
399
|
await this.page.waitForTimeout(200);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
400
|
+
let sent = false;
|
|
401
|
+
for (const sel of SEND_SELECTORS) {
|
|
402
|
+
const btn = this.page.locator(sel).last();
|
|
403
|
+
try {
|
|
404
|
+
if ((await btn.count()) === 0)
|
|
405
|
+
continue;
|
|
406
|
+
if (!(await btn.isVisible().catch(() => false)))
|
|
407
|
+
continue;
|
|
408
|
+
await btn.click({ timeout: 1500 });
|
|
409
|
+
sent = true;
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
catch { }
|
|
409
413
|
}
|
|
410
|
-
|
|
414
|
+
if (!sent) {
|
|
411
415
|
await this.page.keyboard.press('Enter');
|
|
412
416
|
}
|
|
413
417
|
this._lastSentAt = Date.now();
|
|
418
|
+
// Ждём старта: либо появился Stop, либо изменился текст ответа,
|
|
419
|
+
// либо вырос общий объём текста на странице.
|
|
414
420
|
const startDeadline = Date.now() + 15_000;
|
|
421
|
+
const startBodyLen = await this.page
|
|
422
|
+
.evaluate(() => document.body.innerText.length)
|
|
423
|
+
.catch(() => 0);
|
|
415
424
|
let started = false;
|
|
416
425
|
while (Date.now() < startDeadline) {
|
|
417
426
|
const gen = await this._isGenerating();
|
|
418
427
|
const cur = await this._readLastAnswerTextClean().catch(() => '');
|
|
419
|
-
|
|
428
|
+
const bodyLen = await this.page
|
|
429
|
+
.evaluate(() => document.body.innerText.length)
|
|
430
|
+
.catch(() => 0);
|
|
431
|
+
if (gen || (cur && cur !== beforeText) || bodyLen > startBodyLen) {
|
|
420
432
|
started = true;
|
|
421
433
|
break;
|
|
422
434
|
}
|
|
@@ -425,18 +437,20 @@ export class DeepSeekBrowser {
|
|
|
425
437
|
if (!started) {
|
|
426
438
|
throw new Error('Ответ не начал генерироваться за 15с. Возможно, сообщение не отправилось.');
|
|
427
439
|
}
|
|
440
|
+
// Ждём, пока ответ перестанет меняться. Условие "не генерируется"
|
|
441
|
+
// проверяем через рост текста, а НЕ через _isGenerating(): кнопка Stop
|
|
442
|
+
// у DeepSeek ненадёжна, и если она не находится, ответ бы никогда не
|
|
443
|
+
// вернулся.
|
|
428
444
|
const deadline = Date.now() + timeout;
|
|
429
445
|
let last = '';
|
|
430
446
|
let stable = 0;
|
|
431
447
|
this._abort = false;
|
|
432
448
|
while (Date.now() < deadline) {
|
|
433
449
|
if (this._abort) {
|
|
434
|
-
// Пользователь нажал Esc — вернём то, что успело сгенерироваться.
|
|
435
450
|
return last || '(прервано пользователем)';
|
|
436
451
|
}
|
|
437
|
-
const gen = await this._isGenerating();
|
|
438
452
|
const cur = await this._readLastAnswerTextClean().catch(() => '');
|
|
439
|
-
if (cur && cur === last
|
|
453
|
+
if (cur && cur === last) {
|
|
440
454
|
stable++;
|
|
441
455
|
if (stable >= 2)
|
|
442
456
|
return cur;
|
|
@@ -475,7 +489,6 @@ export class DeepSeekBrowser {
|
|
|
475
489
|
});
|
|
476
490
|
return { file: filePath, selectors: report };
|
|
477
491
|
}
|
|
478
|
-
// ---------- список чатов ----------
|
|
479
492
|
async _ensureSidebarOpen() {
|
|
480
493
|
const toggles = [
|
|
481
494
|
'button[aria-label*="sidebar" i]',
|
|
@@ -555,10 +568,12 @@ export class DeepSeekBrowser {
|
|
|
555
568
|
try {
|
|
556
569
|
const url = this.page.url();
|
|
557
570
|
const m = url.match(/\/chat\/s\/([a-zA-Z0-9_-]+)/);
|
|
558
|
-
|
|
571
|
+
if (m)
|
|
572
|
+
return m[1];
|
|
573
|
+
return this._netChatId;
|
|
559
574
|
}
|
|
560
575
|
catch {
|
|
561
|
-
return
|
|
576
|
+
return this._netChatId;
|
|
562
577
|
}
|
|
563
578
|
}
|
|
564
579
|
async close() {
|
|
@@ -570,4 +585,3 @@ export class DeepSeekBrowser {
|
|
|
570
585
|
await killStaleChrome();
|
|
571
586
|
}
|
|
572
587
|
}
|
|
573
|
-
//# sourceMappingURL=browser.js.map
|
package/dist/config.js
CHANGED
package/dist/confirm.js
CHANGED
package/dist/diff.js
CHANGED
package/dist/gitTools.js
CHANGED