zames_pro 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-loop.js +652 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/browser.js +573 -0
- package/dist/browser.js.map +1 -0
- package/dist/config.js +74 -0
- package/dist/config.js.map +1 -0
- package/dist/confirm.js +82 -0
- package/dist/confirm.js.map +1 -0
- package/dist/diff.js +38 -0
- package/dist/diff.js.map +1 -0
- package/dist/gitTools.js +219 -0
- package/dist/gitTools.js.map +1 -0
- package/dist/index.js +1365 -0
- package/dist/index.js.map +1 -0
- package/dist/input.js +524 -0
- package/dist/input.js.map +1 -0
- package/dist/markdown.js +48 -0
- package/dist/markdown.js.map +1 -0
- package/dist/self-review.js +281 -0
- package/dist/self-review.js.map +1 -0
- package/dist/sessions.js +123 -0
- package/dist/sessions.js.map +1 -0
- package/dist/spinner.js +138 -0
- package/dist/spinner.js.map +1 -0
- package/{src → dist}/system-prompt.js +10 -14
- package/dist/system-prompt.js.map +1 -0
- package/{src → dist}/theme.js +17 -19
- package/dist/theme.js.map +1 -0
- package/dist/tools.js +189 -0
- package/dist/tools.js.map +1 -0
- package/dist/transcript.js +51 -0
- package/dist/transcript.js.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/undo.js +100 -0
- package/dist/undo.js.map +1 -0
- package/dist/web.js +220 -0
- package/dist/web.js.map +1 -0
- package/dist/xml-toolcall.js +68 -0
- package/dist/xml-toolcall.js.map +1 -0
- package/package.json +13 -5
- package/src/agent-loop.js +0 -575
- package/src/browser.js +0 -627
- package/src/config.js +0 -81
- package/src/confirm.js +0 -85
- package/src/diff.js +0 -32
- package/src/gitTools.js +0 -256
- package/src/index.js +0 -1461
- package/src/input.js +0 -447
- package/src/markdown.js +0 -49
- package/src/self-review.js +0 -319
- package/src/spinner.js +0 -144
- package/src/tools.js +0 -206
- package/src/transcript.js +0 -46
- package/src/undo.js +0 -101
- package/src/web.js +0 -266
- package/src/xml-toolcall.js +0 -69
package/src/browser.js
DELETED
|
@@ -1,627 +0,0 @@
|
|
|
1
|
-
import { chromium } from 'playwright'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import os from 'os'
|
|
4
|
-
import fs from 'fs/promises'
|
|
5
|
-
import { execSync } from 'child_process'
|
|
6
|
-
import { theme } from './theme.js'
|
|
7
|
-
|
|
8
|
-
const USER_DATA_DIR = path.join(os.homedir(), '.zames', 'profile')
|
|
9
|
-
const CHAT_URL = 'https://chat.deepseek.com/'
|
|
10
|
-
|
|
11
|
-
const INPUT_SELECTORS = [
|
|
12
|
-
'textarea',
|
|
13
|
-
'div[contenteditable="true"]',
|
|
14
|
-
'[role="textbox"]',
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
-
const ANSWER_SELECTORS = [
|
|
18
|
-
'div[class*="ds-markdown"]',
|
|
19
|
-
'div[class*="markdown"]',
|
|
20
|
-
]
|
|
21
|
-
|
|
22
|
-
const STOP_SELECTORS = [
|
|
23
|
-
'button:has-text("Stop")',
|
|
24
|
-
'button:has-text("Остановить")',
|
|
25
|
-
'button[aria-label*="Stop" i]',
|
|
26
|
-
]
|
|
27
|
-
|
|
28
|
-
// Служебные статусы интерфейса DeepSeek, которые НЕ являются ответом модели.
|
|
29
|
-
// Иначе агент принимает статус (Reading...) за ответ и ломает разбор.
|
|
30
|
-
const STATUS_RE = /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// ---------- profile cleanup ----------
|
|
34
|
-
|
|
35
|
-
async function killStaleChrome() {
|
|
36
|
-
if (process.platform !== 'win32') return
|
|
37
|
-
try {
|
|
38
|
-
execSync(
|
|
39
|
-
`powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"Name='chrome.exe'\\" | Where-Object { $_.CommandLine -like '*\\.zames\\profile*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"`,
|
|
40
|
-
{ stdio: 'ignore', timeout: 5000 },
|
|
41
|
-
)
|
|
42
|
-
await new Promise((r) => setTimeout(r, 800))
|
|
43
|
-
} catch {}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function cleanSingletonFiles() {
|
|
47
|
-
const names = ['SingletonLock', 'SingletonCookie', 'SingletonSocket']
|
|
48
|
-
for (const name of names) {
|
|
49
|
-
const p = path.join(USER_DATA_DIR, name)
|
|
50
|
-
try {
|
|
51
|
-
await fs.unlink(p)
|
|
52
|
-
} catch {}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function profileLooksLocked() {
|
|
57
|
-
try {
|
|
58
|
-
await fs.access(path.join(USER_DATA_DIR, 'SingletonLock'))
|
|
59
|
-
return true
|
|
60
|
-
} catch {
|
|
61
|
-
return false
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function nukeProfile() {
|
|
66
|
-
try {
|
|
67
|
-
await fs.rm(USER_DATA_DIR, { recursive: true, force: true })
|
|
68
|
-
} catch {}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ---------- class ----------
|
|
72
|
-
|
|
73
|
-
export class DeepSeekBrowser {
|
|
74
|
-
constructor({
|
|
75
|
-
headless = false,
|
|
76
|
-
debug = false,
|
|
77
|
-
channel = 'chrome',
|
|
78
|
-
answerTimeoutMs = 180000,
|
|
79
|
-
askRetries = 3,
|
|
80
|
-
stabilityChecks = 3,
|
|
81
|
-
stabilityDelayMs = 1000,
|
|
82
|
-
minSendIntervalMs = 25000,
|
|
83
|
-
} = {}) {
|
|
84
|
-
this.headless = headless
|
|
85
|
-
this.debug = debug
|
|
86
|
-
this.channel = channel
|
|
87
|
-
this.answerTimeoutMs = answerTimeoutMs
|
|
88
|
-
this.askRetries = askRetries
|
|
89
|
-
this.stabilityChecks = stabilityChecks
|
|
90
|
-
this.stabilityDelayMs = stabilityDelayMs
|
|
91
|
-
// Минимальный интервал между отправками в чат: DeepSeek ограничивает
|
|
92
|
-
// частоту («Messages too frequent. Try again later.»).
|
|
93
|
-
this.minSendIntervalMs = minSendIntervalMs
|
|
94
|
-
this._lastSentAt = 0
|
|
95
|
-
this.context = null
|
|
96
|
-
this.page = null
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async launch() {
|
|
100
|
-
await killStaleChrome()
|
|
101
|
-
if (await profileLooksLocked()) {
|
|
102
|
-
if (this.debug) console.error('profile: удаляю Singleton-файлы')
|
|
103
|
-
await cleanSingletonFiles()
|
|
104
|
-
}
|
|
105
|
-
await this._launchOnce()
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async _launchOnce() {
|
|
109
|
-
const options = {
|
|
110
|
-
headless: this.headless,
|
|
111
|
-
slowMo: 30,
|
|
112
|
-
args: ['--disable-blink-features=AutomationControlled'],
|
|
113
|
-
}
|
|
114
|
-
if (this.channel) options.channel = this.channel
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
this.context = await chromium.launchPersistentContext(
|
|
118
|
-
USER_DATA_DIR,
|
|
119
|
-
options,
|
|
120
|
-
)
|
|
121
|
-
} catch (e) {
|
|
122
|
-
if (
|
|
123
|
-
/Target page, context or browser has been closed|profile.*in use/i.test(
|
|
124
|
-
e.message,
|
|
125
|
-
)
|
|
126
|
-
) {
|
|
127
|
-
if (this.debug)
|
|
128
|
-
console.error('profile: занят, перезапускаю после очистки')
|
|
129
|
-
await killStaleChrome()
|
|
130
|
-
await cleanSingletonFiles()
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
this.context = await chromium.launchPersistentContext(
|
|
134
|
-
USER_DATA_DIR,
|
|
135
|
-
options,
|
|
136
|
-
)
|
|
137
|
-
} catch (e2) {
|
|
138
|
-
if (this.debug) console.error('profile: сношу целиком')
|
|
139
|
-
await nukeProfile()
|
|
140
|
-
this.context = await chromium.launchPersistentContext(
|
|
141
|
-
USER_DATA_DIR,
|
|
142
|
-
options,
|
|
143
|
-
)
|
|
144
|
-
}
|
|
145
|
-
} else {
|
|
146
|
-
throw e
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
this.page = this.context.pages()[0] || (await this.context.newPage())
|
|
151
|
-
await this.page.goto(CHAT_URL, { waitUntil: 'domcontentloaded' })
|
|
152
|
-
return this
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async restart() {
|
|
156
|
-
try {
|
|
157
|
-
if (this.context) await this.context.close()
|
|
158
|
-
} catch {}
|
|
159
|
-
await this.launch()
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async waitForLogin() {
|
|
163
|
-
const loggedIn = await this.isLoggedIn()
|
|
164
|
-
if (loggedIn) return
|
|
165
|
-
|
|
166
|
-
console.log('\n🔐 Залогиньтесь в DeepSeek в открытом браузере.')
|
|
167
|
-
console.log(' После входа нажмите Enter в терминале...\n')
|
|
168
|
-
|
|
169
|
-
const readline = await import('readline')
|
|
170
|
-
const rl = readline.createInterface({
|
|
171
|
-
input: process.stdin,
|
|
172
|
-
output: process.stdout,
|
|
173
|
-
})
|
|
174
|
-
await new Promise((resolve) => {
|
|
175
|
-
rl.question('', () => {
|
|
176
|
-
rl.close()
|
|
177
|
-
resolve()
|
|
178
|
-
})
|
|
179
|
-
})
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async isLoggedIn() {
|
|
183
|
-
for (const sel of INPUT_SELECTORS) {
|
|
184
|
-
try {
|
|
185
|
-
await this.page.locator(sel).first().waitFor({
|
|
186
|
-
state: 'visible',
|
|
187
|
-
timeout: 3000,
|
|
188
|
-
})
|
|
189
|
-
return true
|
|
190
|
-
} catch {}
|
|
191
|
-
}
|
|
192
|
-
return false
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async newChat() {
|
|
196
|
-
const newChatBtn = this.page
|
|
197
|
-
.locator('button, a')
|
|
198
|
-
.filter({ hasText: /new chat|новый чат|новый диалог/i })
|
|
199
|
-
.first()
|
|
200
|
-
try {
|
|
201
|
-
await newChatBtn.click({ timeout: 2000 })
|
|
202
|
-
await this.page.waitForTimeout(1000)
|
|
203
|
-
} catch {
|
|
204
|
-
await this.page.goto(CHAT_URL, { waitUntil: 'domcontentloaded' })
|
|
205
|
-
await this.page.waitForTimeout(1000)
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async _findVisible(selectors, timeout = 1000) {
|
|
210
|
-
for (const sel of selectors) {
|
|
211
|
-
const loc = this.page.locator(sel).last()
|
|
212
|
-
try {
|
|
213
|
-
await loc.waitFor({ state: 'visible', timeout })
|
|
214
|
-
return loc
|
|
215
|
-
} catch {}
|
|
216
|
-
}
|
|
217
|
-
return null
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async _readLastAnswerText() {
|
|
221
|
-
return await this.page.evaluate((sels) => {
|
|
222
|
-
let el = null
|
|
223
|
-
for (const s of sels) {
|
|
224
|
-
const list = document.querySelectorAll(s)
|
|
225
|
-
if (list.length) el = list[list.length - 1]
|
|
226
|
-
}
|
|
227
|
-
if (!el) return ''
|
|
228
|
-
|
|
229
|
-
const clone = el.cloneNode(true)
|
|
230
|
-
|
|
231
|
-
Array.from(clone.querySelectorAll('pre')).forEach((pre) => {
|
|
232
|
-
const codeEl = pre.querySelector('code')
|
|
233
|
-
const source = codeEl || pre
|
|
234
|
-
const text = (source.textContent || '').replace(/\n$/, '')
|
|
235
|
-
const langMatch = (source.className || '').match(/language-([\w-]+)/)
|
|
236
|
-
const lang = langMatch ? langMatch[1] : ''
|
|
237
|
-
const replacement = document.createTextNode(
|
|
238
|
-
'```' + lang + '\n' + text + '\n```',
|
|
239
|
-
)
|
|
240
|
-
if (pre.parentNode) pre.parentNode.replaceChild(replacement, pre)
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
Array.from(clone.querySelectorAll('code')).forEach((c) => {
|
|
244
|
-
const replacement = document.createTextNode(
|
|
245
|
-
'`' + (c.textContent || '') + '`',
|
|
246
|
-
)
|
|
247
|
-
if (c.parentNode) c.parentNode.replaceChild(replacement, c)
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
// textContent склеивает блоки без переводов строк, из-за чего
|
|
251
|
-
// Markdown-рендер получает одну длинную строку. Обходим DOM сами и
|
|
252
|
-
// расставляем переводы строк / маркеры Markdown по блочным элементам.
|
|
253
|
-
const NL = String.fromCharCode(10)
|
|
254
|
-
const BULLET = String.fromCharCode(45) + ' ' // '- '
|
|
255
|
-
|
|
256
|
-
function domToMarkdown(node) {
|
|
257
|
-
if (node.nodeType === 3) return node.textContent || ''
|
|
258
|
-
if (node.nodeType !== 1) return ''
|
|
259
|
-
const tag = node.tagName.toLowerCase()
|
|
260
|
-
|
|
261
|
-
if (tag === 'br') return NL
|
|
262
|
-
|
|
263
|
-
const inner = Array.from(node.childNodes)
|
|
264
|
-
.map(domToMarkdown)
|
|
265
|
-
.join('')
|
|
266
|
-
|
|
267
|
-
const STAR = String.fromCharCode(42) // '*'
|
|
268
|
-
if (tag === 'strong' || tag === 'b') return STAR + STAR + inner + STAR + STAR
|
|
269
|
-
if (tag === 'em' || tag === 'i') return STAR + inner + STAR
|
|
270
|
-
if (tag === 'del' || tag === 's') return '~~' + inner + '~~'
|
|
271
|
-
if (tag === 'a') {
|
|
272
|
-
const href = node.getAttribute('href') || ''
|
|
273
|
-
return href ? '[' + inner + '](' + href + ')' : inner
|
|
274
|
-
}
|
|
275
|
-
if (/^h[1-6]$/.test(tag)) {
|
|
276
|
-
const level = Number(tag[1])
|
|
277
|
-
const hashes = '#'.repeat(level)
|
|
278
|
-
return NL + NL + hashes + ' ' + inner.trim() + NL + NL
|
|
279
|
-
}
|
|
280
|
-
if (tag === 'li') {
|
|
281
|
-
const text = inner.trim().replace(new RegExp(NL + '+', 'g'), ' ')
|
|
282
|
-
return BULLET + text + NL
|
|
283
|
-
}
|
|
284
|
-
if (tag === 'ul' || tag === 'ol' || tag === 'blockquote') {
|
|
285
|
-
return NL + inner + NL
|
|
286
|
-
}
|
|
287
|
-
if (
|
|
288
|
-
tag === 'p' ||
|
|
289
|
-
tag === 'div' ||
|
|
290
|
-
tag === 'section' ||
|
|
291
|
-
tag === 'article' ||
|
|
292
|
-
tag === 'tr' ||
|
|
293
|
-
tag === 'table'
|
|
294
|
-
) {
|
|
295
|
-
const text = inner.trim()
|
|
296
|
-
return text ? NL + NL + text : ''
|
|
297
|
-
}
|
|
298
|
-
return inner
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const out = domToMarkdown(clone)
|
|
302
|
-
// Схлопываем тройные+ переводы строк до двойных (разделитель блоков).
|
|
303
|
-
return out.replace(new RegExp(NL + '{3,}', 'g'), NL + NL).trim()
|
|
304
|
-
}, ANSWER_SELECTORS)
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
async _readLastAnswerTextClean() {
|
|
308
|
-
const raw = await this._readLastAnswerText().catch(() => '')
|
|
309
|
-
const t = (raw || '').trim()
|
|
310
|
-
if (!t) return ''
|
|
311
|
-
// Отсекаем служебные статусы интерфейса (Reading..., Думаю...).
|
|
312
|
-
if (STATUS_RE.test(t)) return ''
|
|
313
|
-
return raw
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async _isGenerating() {
|
|
317
|
-
const stop = await this._findVisible(STOP_SELECTORS, 300)
|
|
318
|
-
return !!stop
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Прервать текущую генерацию: нажать Stop в интерфейсе.
|
|
322
|
-
// Используется при нажатии Esc пользователем.
|
|
323
|
-
async stopGeneration() {
|
|
324
|
-
this._abort = true
|
|
325
|
-
const btn = await this._findVisible(STOP_SELECTORS, 500)
|
|
326
|
-
if (btn) {
|
|
327
|
-
try {
|
|
328
|
-
await btn.click({ timeout: 1000 })
|
|
329
|
-
return true
|
|
330
|
-
} catch {}
|
|
331
|
-
}
|
|
332
|
-
return false
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
async ask(prompt, { timeout = this.answerTimeoutMs } = {}) {
|
|
336
|
-
let lastErr = null
|
|
337
|
-
|
|
338
|
-
for (let attempt = 1; attempt <= this.askRetries; attempt++) {
|
|
339
|
-
try {
|
|
340
|
-
return await this._askOnce(prompt, { timeout })
|
|
341
|
-
} catch (e) {
|
|
342
|
-
lastErr = e
|
|
343
|
-
console.error(
|
|
344
|
-
`\n⚠ ask() попытка ${attempt}/${this.askRetries} провалилась: ${e.message}`,
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
if (/closed|crash|Target page|browser/i.test(e.message)) {
|
|
348
|
-
console.error('⚠ перезапускаю браузер...')
|
|
349
|
-
try {
|
|
350
|
-
await this.restart()
|
|
351
|
-
await this.waitForLogin()
|
|
352
|
-
} catch (re) {
|
|
353
|
-
console.error(`⚠ не удалось перезапустить: ${re.message}`)
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (attempt < this.askRetries) {
|
|
358
|
-
await new Promise((r) => setTimeout(r, 2000 * attempt))
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
throw new Error(
|
|
364
|
-
`ask() провалился после ${this.askRetries} попыток: ${lastErr?.message}`,
|
|
365
|
-
)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Вставка текста в поле ввода.
|
|
369
|
-
//
|
|
370
|
-
// fill()/insertText() ломаются на многострочном тексте в contenteditable-
|
|
371
|
-
// редакторах (ProseMirror/Lexical/...): символ перевода строки там
|
|
372
|
-
// трактуется как Enter, и в поле остаётся только первая строка. Поэтому
|
|
373
|
-
// для contenteditable и [role=textbox] эмулируем paste-событие с полным
|
|
374
|
-
// текстом — то же, что делает Shift+Insert. Для нативных textarea/input
|
|
375
|
-
// перевод строки работает и так.
|
|
376
|
-
async _setInputText(input, text) {
|
|
377
|
-
const tag = await input.evaluate((el) => el.tagName.toLowerCase())
|
|
378
|
-
const isNative = tag === 'textarea' || tag === 'input'
|
|
379
|
-
|
|
380
|
-
if (isNative) {
|
|
381
|
-
try {
|
|
382
|
-
await input.fill(text, { timeout: 5000 })
|
|
383
|
-
return
|
|
384
|
-
} catch {}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
await input.click()
|
|
388
|
-
// Выделяем всё содержимое, чтобы вставка заменила его целиком.
|
|
389
|
-
await this.page.keyboard.press('Control+A')
|
|
390
|
-
await this.page.keyboard.press('Delete')
|
|
391
|
-
|
|
392
|
-
const ok = await input.evaluate((el, value) => {
|
|
393
|
-
el.focus()
|
|
394
|
-
const dt = new DataTransfer()
|
|
395
|
-
dt.setData('text/plain', value)
|
|
396
|
-
const ev = new ClipboardEvent('paste', {
|
|
397
|
-
clipboardData: dt,
|
|
398
|
-
bubbles: true,
|
|
399
|
-
cancelable: true,
|
|
400
|
-
})
|
|
401
|
-
el.dispatchEvent(ev)
|
|
402
|
-
// Если обработчик не отменил вставку и поле осталось пустым —
|
|
403
|
-
// пробуем через insertText вручную (fallback ниже).
|
|
404
|
-
return true
|
|
405
|
-
}, text)
|
|
406
|
-
|
|
407
|
-
if (!ok) {
|
|
408
|
-
await this.page.keyboard.insertText(text)
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Проверяем, что текст реально попал в поле. Если редактор проигнорировал
|
|
412
|
-
// paste — падаем на insertText.
|
|
413
|
-
const got = await input.evaluate((el) => {
|
|
414
|
-
if (el.tagName.toLowerCase() === 'textarea' || el.tagName.toLowerCase() === 'input') {
|
|
415
|
-
return el.value
|
|
416
|
-
}
|
|
417
|
-
return el.innerText || el.textContent || ''
|
|
418
|
-
})
|
|
419
|
-
const norm = (s) =>
|
|
420
|
-
(s || '').replace(/\r\n/g, '\n').replace(/\u00a0/g, ' ').trim()
|
|
421
|
-
if (!norm(got)) {
|
|
422
|
-
await input.click()
|
|
423
|
-
await this.page.keyboard.insertText(text)
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Ждём, пока с прошлой отправки пройдёт minSendIntervalMs. Защита от
|
|
428
|
-
// «Messages too frequent. Try again later.» на [chat.deepseek.com](https://chat.deepseek.com/).
|
|
429
|
-
async _waitForSendSlot() {
|
|
430
|
-
if (!this._lastSentAt) return // первая отправка — пауза не нужна
|
|
431
|
-
const gap = this.minSendIntervalMs - (Date.now() - this._lastSentAt)
|
|
432
|
-
if (gap <= 0) return
|
|
433
|
-
console.error(
|
|
434
|
-
theme.warn(
|
|
435
|
-
`⏳ пауза ${Math.ceil(gap / 1000)}с перед отправкой (лимит частоты DeepSeek)...`,
|
|
436
|
-
),
|
|
437
|
-
)
|
|
438
|
-
await this.page.waitForTimeout(gap)
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
async _askOnce(prompt, { timeout }) {
|
|
442
|
-
const input = await this._findVisible(INPUT_SELECTORS, 10_000)
|
|
443
|
-
if (!input) {
|
|
444
|
-
throw new Error(
|
|
445
|
-
'Не найдено поле ввода. Запустите /debug-dom и поправьте INPUT_SELECTORS.',
|
|
446
|
-
)
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const beforeText = await this._readLastAnswerTextClean().catch(() => '')
|
|
450
|
-
|
|
451
|
-
await this._waitForSendSlot()
|
|
452
|
-
await this._setInputText(input, prompt)
|
|
453
|
-
await this.page.waitForTimeout(200)
|
|
454
|
-
|
|
455
|
-
const sendBtn = this.page
|
|
456
|
-
.locator('button')
|
|
457
|
-
.filter({ hasText: /send|отправить/i })
|
|
458
|
-
.first()
|
|
459
|
-
try {
|
|
460
|
-
await sendBtn.click({ timeout: 1500 })
|
|
461
|
-
} catch {
|
|
462
|
-
await this.page.keyboard.press('Enter')
|
|
463
|
-
}
|
|
464
|
-
this._lastSentAt = Date.now()
|
|
465
|
-
|
|
466
|
-
const startDeadline = Date.now() + 15_000
|
|
467
|
-
let started = false
|
|
468
|
-
while (Date.now() < startDeadline) {
|
|
469
|
-
const gen = await this._isGenerating()
|
|
470
|
-
const cur = await this._readLastAnswerTextClean().catch(() => '')
|
|
471
|
-
if (gen || (cur && cur !== beforeText)) {
|
|
472
|
-
started = true
|
|
473
|
-
break
|
|
474
|
-
}
|
|
475
|
-
await this.page.waitForTimeout(300)
|
|
476
|
-
}
|
|
477
|
-
if (!started) {
|
|
478
|
-
throw new Error(
|
|
479
|
-
'Ответ не начал генерироваться за 15с. Возможно, сообщение не отправилось.',
|
|
480
|
-
)
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const deadline = Date.now() + timeout
|
|
484
|
-
let last = ''
|
|
485
|
-
let stable = 0
|
|
486
|
-
this._abort = false
|
|
487
|
-
while (Date.now() < deadline) {
|
|
488
|
-
if (this._abort) {
|
|
489
|
-
// Пользователь нажал Esc — вернём то, что успело сгенерироваться.
|
|
490
|
-
return last || '(прервано пользователем)'
|
|
491
|
-
}
|
|
492
|
-
const gen = await this._isGenerating()
|
|
493
|
-
const cur = await this._readLastAnswerTextClean().catch(() => '')
|
|
494
|
-
if (cur && cur === last && !gen) {
|
|
495
|
-
stable++
|
|
496
|
-
if (stable >= 2) return cur
|
|
497
|
-
} else {
|
|
498
|
-
stable = 0
|
|
499
|
-
}
|
|
500
|
-
last = cur
|
|
501
|
-
await this.page.waitForTimeout(800)
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (last) return last
|
|
505
|
-
throw new Error('Таймаут ожидания ответа. Попробуйте /debug-dom.')
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
async dumpDom(filePath) {
|
|
509
|
-
if (!this.page) throw new Error('браузер не запущен')
|
|
510
|
-
const html = await this.page.content()
|
|
511
|
-
await fs.writeFile(filePath, html, 'utf-8')
|
|
512
|
-
|
|
513
|
-
const report = await this.page.evaluate(
|
|
514
|
-
(sels) => {
|
|
515
|
-
const result = { answers: {}, stops: {}, inputs: {} }
|
|
516
|
-
for (const s of sels.answers) {
|
|
517
|
-
result.answers[s] = document.querySelectorAll(s).length
|
|
518
|
-
}
|
|
519
|
-
for (const s of sels.stops) {
|
|
520
|
-
result.stops[s] = document.querySelectorAll(s).length
|
|
521
|
-
}
|
|
522
|
-
for (const s of sels.inputs) {
|
|
523
|
-
result.inputs[s] = document.querySelectorAll(s).length
|
|
524
|
-
}
|
|
525
|
-
return result
|
|
526
|
-
},
|
|
527
|
-
{
|
|
528
|
-
answers: ANSWER_SELECTORS,
|
|
529
|
-
stops: STOP_SELECTORS,
|
|
530
|
-
inputs: INPUT_SELECTORS,
|
|
531
|
-
},
|
|
532
|
-
)
|
|
533
|
-
|
|
534
|
-
return { file: filePath, selectors: report }
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// ---------- список чатов ----------
|
|
538
|
-
|
|
539
|
-
async _ensureSidebarOpen() {
|
|
540
|
-
const toggles = [
|
|
541
|
-
'button[aria-label*="sidebar" i]',
|
|
542
|
-
'button[aria-label*="история" i]',
|
|
543
|
-
'button[aria-label*="history" i]',
|
|
544
|
-
'button[class*="sidebar-toggle"]',
|
|
545
|
-
'button[class*="sidebarToggle"]',
|
|
546
|
-
]
|
|
547
|
-
for (const sel of toggles) {
|
|
548
|
-
try {
|
|
549
|
-
const btn = this.page.locator(sel).first()
|
|
550
|
-
if ((await btn.count()) === 0) continue
|
|
551
|
-
if (!(await btn.isVisible().catch(() => false))) continue
|
|
552
|
-
await btn.click({ timeout: 1500 })
|
|
553
|
-
await this.page.waitForTimeout(500)
|
|
554
|
-
return
|
|
555
|
-
} catch {}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
async listChats(limit = 30) {
|
|
560
|
-
await this._ensureSidebarOpen()
|
|
561
|
-
return await this.page.evaluate((lim) => {
|
|
562
|
-
const out = []
|
|
563
|
-
const seen = new Set()
|
|
564
|
-
const anchors = document.querySelectorAll('a[href*="/chat/"]')
|
|
565
|
-
for (const a of anchors) {
|
|
566
|
-
const href = a.getAttribute('href') || ''
|
|
567
|
-
const m =
|
|
568
|
-
href.match(/\/chat\/s\/([a-zA-Z0-9_-]+)/) ||
|
|
569
|
-
href.match(/\/a\/chat\/s\/([a-zA-Z0-9_-]+)/)
|
|
570
|
-
if (!m) continue
|
|
571
|
-
const id = m[1]
|
|
572
|
-
if (seen.has(id)) continue
|
|
573
|
-
seen.add(id)
|
|
574
|
-
|
|
575
|
-
const titleEl = a.querySelector('[class*="title"], [class*="text"]')
|
|
576
|
-
let title = (titleEl ? titleEl.textContent : a.textContent) || ''
|
|
577
|
-
title = title.trim().replace(/\s+/g, ' ')
|
|
578
|
-
if (!title) title = '(без названия)'
|
|
579
|
-
|
|
580
|
-
out.push({ id, title, href })
|
|
581
|
-
if (out.length >= lim) break
|
|
582
|
-
}
|
|
583
|
-
return out
|
|
584
|
-
}, limit)
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
async openChat(id) {
|
|
588
|
-
const candidates = [`a[href$="/chat/s/${id}"]`, `a[href*="${id}"]`]
|
|
589
|
-
for (const sel of candidates) {
|
|
590
|
-
try {
|
|
591
|
-
const loc = this.page.locator(sel).first()
|
|
592
|
-
if ((await loc.count()) === 0) continue
|
|
593
|
-
await loc.click({ timeout: 3000 })
|
|
594
|
-
await this.page.waitForTimeout(1500)
|
|
595
|
-
return true
|
|
596
|
-
} catch {}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
try {
|
|
600
|
-
await this.page.goto(`https://chat.deepseek.com/a/chat/s/${id}`, {
|
|
601
|
-
waitUntil: 'domcontentloaded',
|
|
602
|
-
timeout: 20_000,
|
|
603
|
-
})
|
|
604
|
-
await this.page.waitForTimeout(1500)
|
|
605
|
-
return true
|
|
606
|
-
} catch (e) {
|
|
607
|
-
throw new Error(`Не удалось открыть чат ${id}: ${e.message}`)
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
async getCurrentChatId() {
|
|
612
|
-
try {
|
|
613
|
-
const url = this.page.url()
|
|
614
|
-
const m = url.match(/\/chat\/s\/([a-zA-Z0-9_-]+)/)
|
|
615
|
-
return m ? m[1] : null
|
|
616
|
-
} catch {
|
|
617
|
-
return null
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
async close() {
|
|
622
|
-
try {
|
|
623
|
-
if (this.context) await this.context.close()
|
|
624
|
-
} catch {}
|
|
625
|
-
await killStaleChrome()
|
|
626
|
-
}
|
|
627
|
-
}
|
package/src/config.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import fs from 'fs'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import os from 'os'
|
|
4
|
-
|
|
5
|
-
const ZAMES_HOME = path.join(os.homedir(), '.zames')
|
|
6
|
-
const HOME_CONFIG = path.join(ZAMES_HOME, 'config.json')
|
|
7
|
-
const PROJECT_CONFIG = path.join(process.cwd(), '.zamesrc.json')
|
|
8
|
-
|
|
9
|
-
const DEFAULTS = {
|
|
10
|
-
maxIterations: 40,
|
|
11
|
-
headless: false,
|
|
12
|
-
debug: false,
|
|
13
|
-
browserChannel: null,
|
|
14
|
-
|
|
15
|
-
confirmation: {
|
|
16
|
-
write: true,
|
|
17
|
-
edit: true,
|
|
18
|
-
bash: true,
|
|
19
|
-
alwaysConfirm: [
|
|
20
|
-
'rm\\s+-rf',
|
|
21
|
-
'rmdir\\s+/s',
|
|
22
|
-
'del\\s+/[sqf]',
|
|
23
|
-
'format\\s+[a-z]:',
|
|
24
|
-
'shutdown',
|
|
25
|
-
'reg\\s+delete',
|
|
26
|
-
'remove-item.*-recurse',
|
|
27
|
-
'git\\s+push\\s+--force',
|
|
28
|
-
],
|
|
29
|
-
},
|
|
30
|
-
|
|
31
|
-
undo: {
|
|
32
|
-
enabled: true,
|
|
33
|
-
maxBackups: 200,
|
|
34
|
-
},
|
|
35
|
-
|
|
36
|
-
transcript: {
|
|
37
|
-
enabled: true,
|
|
38
|
-
dir: path.join(os.homedir(), '.zames', 'logs'),
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
browser: {
|
|
42
|
-
answerTimeoutMs: 180000,
|
|
43
|
-
askRetries: 3,
|
|
44
|
-
stabilityChecks: 3,
|
|
45
|
-
stabilityDelayMs: 1000,
|
|
46
|
-
minSendIntervalMs: 25000,
|
|
47
|
-
},
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function loadConfig() {
|
|
51
|
-
const merged = JSON.parse(JSON.stringify(DEFAULTS))
|
|
52
|
-
for (const file of [HOME_CONFIG, PROJECT_CONFIG]) {
|
|
53
|
-
try {
|
|
54
|
-
const data = JSON.parse(fs.readFileSync(file, 'utf-8'))
|
|
55
|
-
deepMerge(merged, data)
|
|
56
|
-
} catch (e) {
|
|
57
|
-
if (e.code !== 'ENOENT') {
|
|
58
|
-
console.error(`config: не удалось прочитать ${file}: ${e.message}`)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return merged
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function deepMerge(target, source) {
|
|
66
|
-
for (const key of Object.keys(source)) {
|
|
67
|
-
if (
|
|
68
|
-
source[key] &&
|
|
69
|
-
typeof source[key] === 'object' &&
|
|
70
|
-
!Array.isArray(source[key])
|
|
71
|
-
) {
|
|
72
|
-
target[key] = target[key] || {}
|
|
73
|
-
deepMerge(target[key], source[key])
|
|
74
|
-
} else {
|
|
75
|
-
target[key] = source[key]
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export const CONFIG_PATHS = { HOME_CONFIG, PROJECT_CONFIG }
|
|
81
|
-
export { ZAMES_HOME }
|