From 263ad14810367c4152384590387e430a67e93818 Mon Sep 17 00:00:00 2001 From: Alex Cube Date: Wed, 26 Aug 2026 14:13:57 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=83=D0=BB=D1=8C=D1=82:=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=BA=D0=BB=D1=8E=D1=87=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=BE?= =?UTF-8?q?=D0=B2,=20=D0=BF=D0=BE=D0=BC=D0=BE=D1=89=D1=8C=20=D1=81=20git,?= =?UTF-8?q?=20=D0=BF=D0=B0=D0=BC=D1=8F=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=88=D0=BB=D0=BE=D0=B3=D0=BE=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Три вещи, которых не хватало по сравнению с пультом DeepShim, — теперь здесь, и DeepShim можно закрывать. 1. Выключатель инструментов. Снят — приставка, дерево папки и напоминание про NEED/LIST/FIND не едут вовсе, круг ровно один: обычное "спросил по этим файлам, получил ответ". Замер на одной задаче: 229 символов вопроса против 1886. В консоли это флаг --no-tools. 2. Помощь с git: ряд всплывает, только если в папке нет репозитория или в .gitignore не хватает строк. Главная - backup/, куда ложатся прежние версии файлов перед записью. Перенесено из пульта DeepShim. 3. "Помнить прошлый ответ": галочка, по которой прошлый ответ уезжает вместе со следующим вопросом. Разговор из нескольких вопросов, которого раньше не было: стенограмма велась внутри одного вопроса и на этом всё. Попутно два дефекта, оба найдены при проверке: - Разбор флагов съедал аргумент после булева флага: "deepwork --apply вопрос" превращался в apply="вопрос", а вопрос пропадал. Молчало, пока булев флаг стоял последним. Та же грабля уже ловилась в ask.mjs; добавлен BOOL_FLAGS. - Блок-запрос вплотную к прозе не распознавался. DeepSeek регулярно пишет "Запрашиваю поиск константы.=== FIND: MAGIC ===" одной строкой, разбор требовал начала строки, и круг уходил впустую. Якорь начала снят, якорь конца оставлен: упоминание формата посреди фразы запросом не считается. От запросов внутри === FILE: === бережёт вырезание этих блоков, а не якорь. Проверено вживую: без инструментов - 229 символов, один круг, верный ответ; с инструментами - FIND распознан, два круга, верный ответ; git - repo:false и пять недостающих строк, после кнопки repo:true и missing пуст; память - "удвой это число" вернуло 80834 против бессмыслицы без галочки. --selftest дополнен двумя случаями на разбор запросов. Co-Authored-By: Claude Opus 5 --- README.md | 45 +++++++++++++++ deepwork.mjs | 154 ++++++++++++++++++++++++++++++++++++++++++--------- ui.html | 52 ++++++++++++++++- 3 files changed, 223 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 820d124..e27525a 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,19 @@ node deepwork.mjs --check - ход работы построчно: видно, что DeepSeek запросил и сколько это заняло; - у каждого пришедшего файла кнопка «Записать». +Три переключателя рядом с кнопкой «Спросить»: + +| | Что делает | +|---|---| +| **инструменты** | разрешить модели самой запрашивать файлы. Снимите — получится обычный «спросил по этим файлам, получил ответ»: один круг, без беготни по папке. См. [когда выключать](#когда-выключать-инструменты) | +| **шагов** | сколько кругов позволить. Без инструментов не нужен и гаснет | +| **помнить прошлый ответ** | прошлый ответ уедет вместе со следующим вопросом. Так можно вести разговор: «а теперь перепиши это на async» | + +Внизу слева иногда всплывает ряд про **git** — он появляется, только если +в папке нет репозитория или в `.gitignore` не хватает строк. Главная из них — +`backup/`: туда ложатся прежние версии файлов перед записью, и однажды они +уедут в коммит. + Пульт открывается в браузере **по умолчанию**, а не в том, которым управляет `deepseek-api`: иначе он занял бы там вкладку, и вкладка с чатом потерялась бы. Порт 8787. @@ -150,10 +163,23 @@ node C:\#Projects\deepwork\deepwork.mjs "почему падает загруз | `--files a.js,b.js` | положить эти файлы в вопрос сразу | | `--steps 6` | сколько кругов позволить (по умолчанию 4) | | `--apply` | записать пришедшие файлы на диск (без него только покажет) | +| `--no-tools` | не давать модели инструменты: один круг, только отмеченные файлы | | `--chat search` | режим с поиском в интернете вместо обычного | | `--check` | проверить связку и выйти | | `--selftest` | внутренние проверки, ничего не отправляя | +### Когда выключать инструменты + +Инструменты хороши, когда **непонятно, что читать**: «почему падает загрузка» +в чужом проекте. Модель сама найдёт нужное. + +Выключать стоит, когда вы **точно знаете нужные файлы**. Тогда: + +- ответ приходит за один круг вместо двух-трёх; +- вопрос выходит вчетверо короче — приставка, дерево папки и напоминание + не едут вовсе (замер: 229 символов против 1886 на той же задаче); +- модель не ходит по папке без нужды, и наружу не уезжает лишний код. + ### Чего ожидать по времени Один круг — 7–15 секунд. Обычная задача укладывается в два-три круга, то есть @@ -374,6 +400,25 @@ DeepSeek видел, что данные неполные. живут в его `browser.mjs`. Отсюда остаётся одно требование — **не сворачивать окно браузера**. +### Блок-запрос вплотную к прозе, 26.08.2026 + +DeepSeek регулярно дописывает запрос в конец фразы, без перевода строки: + +``` +Мне нужны данные для ответа. Запрашиваю поиск константы MAGIC.=== FIND: MAGIC === +``` + +Разбор требовал, чтобы блок начинал строку, и такой запрос не видел: цикл +решал, что запросов нет, и заканчивал работу догадкой. Круг впустую. + +Якорь начала строки снят, якорь конца оставлен: упоминание формата посреди +фразы («верни `=== NEED: файл ===` и я пришлю») запросом по-прежнему не +считается. От запросов внутри блоков `=== FILE: ===` бережёт не якорь, +а вырезание этих блоков перед разбором — так было и раньше. + +Проверено на живой задаче: до правки — один круг и «запросов нет»; после — +`FIND: MAGIC`, второй круг, верный ответ. + ### Переезд на deepseek-api, 26.08.2026 Раньше `deepwork` вызывал мост `ask.mjs` из проекта DeepShim дочерним процессом diff --git a/deepwork.mjs b/deepwork.mjs index cc713f7..73953e7 100644 --- a/deepwork.mjs +++ b/deepwork.mjs @@ -18,6 +18,13 @@ const HERE = dirname(fileURLToPath(import.meta.url)); // ------------------------------------------------------------------ аргументы +// Флаги без значения. Без этого списка разбор съедал следующий аргумент: +// `deepwork --apply "вопрос"` превращался в apply="вопрос", а сам вопрос +// пропадал. Молчит это до тех пор, пока булев флаг стоит последним, — а +// стоило поставить его перед вопросом, и команда падала на пустом вопросе. +// Та же грабля уже ловилась в ask.mjs проекта DeepShim. +const BOOL_FLAGS = new Set(['apply', 'check', 'selftest', 'ui', 'no-open', 'no-tools']); + const rawArgs = process.argv.slice(2); const positional = []; const flags = {}; @@ -26,9 +33,11 @@ for (let i = 0; i < rawArgs.length; i++) { if (a.startsWith('--')) { const eq = a.indexOf('='); if (eq !== -1) { flags[a.slice(2, eq)] = a.slice(eq + 1); continue; } + const name = a.slice(2); const next = rawArgs[i + 1]; - if (next !== undefined && !next.startsWith('--')) { flags[a.slice(2)] = next; i++; } - else flags[a.slice(2)] = true; + if (!BOOL_FLAGS.has(name) && next !== undefined && !next.startsWith('--')) { + flags[name] = next; i++; + } else flags[name] = true; } else positional.push(a); } @@ -44,6 +53,8 @@ deepwork — задать DeepSeek вопрос и дать ему дочита --steps N сколько кругов позволить (по умолчанию 4) --apply применить пришедшие блоки === FILE: === на диск (прежняя версия уезжает в backup/) + --no-tools не давать модели инструменты: один круг, только + отмеченные файлы. Так работает пульт «с галочками» --chat роль think (по умолчанию) | search | vision --timeout сек сколько секунд ждать ответа на один шаг --check проверить, что сервис на месте и вкладка жива @@ -166,7 +177,14 @@ function parsePatches(answer) { function parseRequests(answer) { const text = String(answer || '').replace(FILE_BLOCK_RE, ''); const out = []; - const re = /^=== (NEED|LIST|FIND):\s*(.+?)\s*===\s*$/gm; + // Блок не обязан начинать строку: DeepSeek регулярно дописывает его вплотную + // к прозе — «Запрашиваю поиск константы.=== FIND: MAGIC ===». С якорем ^ такой + // запрос не виделся, и круг уходил впустую (замер 26.08.2026). + // Конец строки якорем ОСТАЁТСЯ: иначе упоминание формата посреди фразы + // («верни === NEED: файл === и я пришлю») стало бы настоящим запросом. + // От запросов внутри блоков === FILE: === бережёт не якорь, а вырезание + // этих блоков строкой выше. + const re = /=== (NEED|LIST|FIND):\s*(.+?)\s*===\s*$/gm; let m; while ((m = re.exec(text))) out.push({ kind: m[1], arg: m[2] }); return out; @@ -291,33 +309,49 @@ function isServerBusy(text) { // ------------------------------------------------------------------ цикл -async function buildFirst(root, files, question) { - let head = ''; - try { - const raw = await readFile(join(HERE, 'agent-prompt.md'), 'utf8'); - head = (raw.split(/^---$/m)[1] ?? '').trim(); - } catch { /* нет файла — поедет без приставки, инструменты просто не всплывут */ } +// tools=false — режим «просто ответь по этим файлам»: ни приставки, ни дерева, +// ни напоминания про NEED/LIST/FIND. Тогда и круг ровно один. Инструменты +// хороши, когда неизвестно, что читать; когда файлы выбраны руками, они только +// тратят время и подбивают модель ходить по папке без нужды. +async function buildFirst(root, files, question, { tools = true, context = '' } = {}) { + const parts = []; + + if (tools) { + let head = ''; + try { + const raw = await readFile(join(HERE, 'agent-prompt.md'), 'utf8'); + head = (raw.split(/^---$/m)[1] ?? '').trim(); + } catch { /* нет файла — поедет без приставки, инструменты просто не всплывут */ } + + // Дерево папки едет сразу. Первый живой прогон без него: DeepSeek угадывал + // имена (server.js, bridge.js — таких нет), потом просил LIST *.js, потом + // LIST **/* — два круга из четырёх ушли на выяснение, что вообще лежит в + // папке. Дерево этого проекта — 600 символов против двух минут ожидания. + const tree = (await listFiles(root)).slice(0, LIST_PATHS); + parts.push(head, '', + '=== RESULT: LIST **/* ===', + fence(tree.map(f => `${f.path} ${f.size} б`).join('\n')), + ''); + } - // Дерево папки едет сразу. Первый живой прогон без него: DeepSeek угадывал - // имена (server.js, bridge.js — таких нет), потом просил LIST *.js, потом - // LIST **/* — два круга из четырёх ушли на выяснение, что вообще лежит в - // папке. Дерево этого проекта — 600 символов против двух минут ожидания. - const tree = (await listFiles(root)).slice(0, LIST_PATHS); - const parts = [head, '', - '=== RESULT: LIST **/* ===', - fence(tree.map(f => `${f.path} ${f.size} б`).join('\n')), - '', - ]; for (const rel of files) { const full = resolveInside(root, rel); if (!full) throw new Error(`файл вне рабочей папки: ${rel}`); parts.push(`=== FILE: ${rel} ===`, fence(await readFile(full, 'utf8')), FILE_CLOSE, ''); } + + // Прошлый ответ, если человек попросил его сохранить. Метки те же, что + // у стенограммы кругов: формат один — модели проще. + if (String(context).trim()) { + parts.push('=== ТВОЙ ПРЕДЫДУЩИЙ ОТВЕТ ===', String(context).trim(), '=== END ===', ''); + } + // Напоминание повторяется ВПЛОТНУЮ к вопросу. Первый живой прогон: приставка // ушла в начало сообщения, между ней и вопросом легли 22 КБ файла, и DeepSeek // инструменты проигнорировал — сам написал «точный код в другом файле» и // выдал три догадки вместо запроса. Инструкция в хвосте весит больше. - parts.push(TAIL_REMINDER, '', 'ВОПРОС:', question.trim()); + if (tools) parts.push(TAIL_REMINDER, ''); + parts.push('ВОПРОС:', question.trim()); return parts.join('\n'); } @@ -357,13 +391,18 @@ function growTranscript(prev, answer, next) { next].join('\n'); } -async function runAgent(cfg, root, question, files, log) { - let text = await buildFirst(root, files, question); +async function runAgent(cfg, root, question, files, log, { tools = true, context = '' } = {}) { + let text = await buildFirst(root, files, question, { tools, context }); let answer = ''; let lastSig = ''; - for (let n = 1; n <= cfg.steps; n++) { + // Без инструментов круг ровно один: запрашивать модели нечем, а блок NEED, + // случайно попавший в текст ответа, запускать круг не должен. + const steps = tools ? cfg.steps : 1; + + for (let n = 1; n <= steps; n++) { answer = await callDeepseek(cfg, text, { n, log }); + if (!tools) break; const requests = parseRequests(answer); if (!requests.length) { log(`шаг ${n}: запросов нет, готово`); break; } @@ -373,7 +412,7 @@ async function runAgent(cfg, root, question, files, log) { if (sig === lastSig) { log(`шаг ${n}: тот же запрос повторно — петля, выхожу`); break; } lastSig = sig; - if (n === cfg.steps) { log(`шаг ${n}: шаги кончились, а запросы ещё идут`); break; } + if (n === steps) { log(`шаг ${n}: шаги кончились, а запросы ещё идут`); break; } log(`шаг ${n}: запрошено ${requests.length}`); const result = await runRequests(root, requests, log); @@ -401,6 +440,54 @@ async function applyPatch(root, rel, content) { return { ok: true, backup }; } +// --------------------------------------------------------------- помощь с git + +// Строки, без которых правки из ответа однажды уедут в коммит. Главная — +// backup/: туда ложатся прежние версии файлов перед записью. +const GITIGNORE_LINES = ['backup/', 'node_modules/', '*.log', 'Thumbs.db', '.DS_Store']; + +async function gitState(root) { + let repo = false; + try { repo = (await stat(join(root, '.git'))).isDirectory(); } catch { /* нет */ } + // Репозиторий может быть заведён давно, а backup/ в .gitignore не попасть — + // тогда кнопка всё ещё нужна, но уже для дополнения. + let text = ''; + try { text = await readFile(join(root, '.gitignore'), 'utf8'); } catch { /* нет файла */ } + const have = new Set(text.split(/\r?\n/).map(l => l.trim())); + return { repo, missing: GITIGNORE_LINES.filter(l => !have.has(l)) }; +} + +async function gitInit(root) { + const { execFile } = await import('node:child_process'); + const run = (cmd, args) => new Promise(resolve => { + execFile(cmd, args, { cwd: root }, (err, out, errOut) => + resolve({ ok: !err, out: String(out || '').trim(), + err: String(errOut || err && err.message || '').trim() })); + }); + + const ver = await run('git', ['--version']); + if (!ver.ok) return { ok: false, reason: 'git не найден в PATH' }; + + const { repo } = await gitState(root); + if (!repo) { + const init = await run('git', ['init']); + if (!init.ok) return { ok: false, reason: init.err || 'git init не выполнился' }; + } + + // .gitignore дополняем, а не перезаписываем: в папке мог быть свой. + const file = join(root, '.gitignore'); + let text = ''; + try { text = await readFile(file, 'utf8'); } catch { /* заведём новый */ } + const have = new Set(text.split(/\r?\n/).map(l => l.trim())); + const missing = GITIGNORE_LINES.filter(l => !have.has(l)); + if (missing.length) { + const head = text && !text.endsWith('\n') ? '\n' : ''; + const note = text ? '\n# добавлено пультом\n' : '# .gitignore заведён пультом\n'; + await writeFile(file, text + head + note + missing.join('\n') + '\n', 'utf8'); + } + return { ok: true, added: missing, wasRepo: repo }; +} + // ------------------------------------------------------------------ пульт // Ход работы копится здесь, а страница забирает его опросом раз в 2 секунды. @@ -451,6 +538,12 @@ async function serve(cfg) { } catch { return json(res, 200, { service: false, deepseek: false }); } } + if (req.method === 'GET' && url.pathname === '/api/git') + return json(res, 200, await gitState(root)); + + if (req.method === 'POST' && url.pathname === '/api/git/init') + return json(res, 200, await gitInit(root)); + if (req.method === 'GET' && url.pathname === '/api/progress') return json(res, 200, { ...progress, seconds: progress.busy ? Math.round((Date.now() - progress.started) / 1000) : 0 }); @@ -463,7 +556,8 @@ async function serve(cfg) { try { const run = { ...cfg, steps: Number.parseInt(body.steps, 10) || cfg.steps }; const answer = await runAgent(run, root, String(body.question), body.files || [], - m => progress.steps.push(m)); + m => progress.steps.push(m), + { tools: body.tools !== false, context: String(body.context || '') }); return json(res, 200, { answer, steps: progress.steps, patches: parsePatches(answer) }); } finally { progress.busy = false; } } @@ -523,6 +617,13 @@ async function selftest() { const req = parseRequests(sample); assert.deepEqual(req.map(r => r.kind), ['NEED', 'LIST', 'FIND']); + + // Блок вплотную к прозе — тоже запрос: так DeepSeek отвечает регулярно. + assert.deepEqual( + parseRequests('Мне нужны данные.=== FIND: MAGIC ===').map(r => r.arg), ['MAGIC']); + // А упоминание формата ПОСРЕДИ фразы запросом быть не должно. + assert.deepEqual( + parseRequests('верни === NEED: файл === и я пришлю его тебе'), []); assert.equal(req[0].arg, 'ask.mjs, ui.html'); const pat = parsePatches(sample); @@ -582,7 +683,8 @@ if (flags.ui || positional[0] === 'ui') { try { console.log(`папка: ${root}`); - const answer = await runAgent(cfg, root, question, files, log); + const answer = await runAgent(cfg, root, question, files, log, + { tools: !flags['no-tools'] }); console.log('\n----- ответ DeepSeek -----'); console.log(answer); diff --git a/ui.html b/ui.html index a324cfd..91fbde7 100644 --- a/ui.html +++ b/ui.html @@ -38,6 +38,10 @@
загружаю…
ничего не выбрано
+
@@ -47,8 +51,12 @@
- + +
@@ -56,6 +64,7 @@