Пульт: выключатель инструментов, помощь с git, память прошлого ответа
Три вещи, которых не хватало по сравнению с пультом 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f34e07e807
commit
263ad14810
@@ -128,6 +128,19 @@ node deepwork.mjs --check
|
|||||||
- ход работы построчно: видно, что DeepSeek запросил и сколько это заняло;
|
- ход работы построчно: видно, что DeepSeek запросил и сколько это заняло;
|
||||||
- у каждого пришедшего файла кнопка «Записать».
|
- у каждого пришедшего файла кнопка «Записать».
|
||||||
|
|
||||||
|
Три переключателя рядом с кнопкой «Спросить»:
|
||||||
|
|
||||||
|
| | Что делает |
|
||||||
|
|---|---|
|
||||||
|
| **инструменты** | разрешить модели самой запрашивать файлы. Снимите — получится обычный «спросил по этим файлам, получил ответ»: один круг, без беготни по папке. См. [когда выключать](#когда-выключать-инструменты) |
|
||||||
|
| **шагов** | сколько кругов позволить. Без инструментов не нужен и гаснет |
|
||||||
|
| **помнить прошлый ответ** | прошлый ответ уедет вместе со следующим вопросом. Так можно вести разговор: «а теперь перепиши это на async» |
|
||||||
|
|
||||||
|
Внизу слева иногда всплывает ряд про **git** — он появляется, только если
|
||||||
|
в папке нет репозитория или в `.gitignore` не хватает строк. Главная из них —
|
||||||
|
`backup/`: туда ложатся прежние версии файлов перед записью, и однажды они
|
||||||
|
уедут в коммит.
|
||||||
|
|
||||||
Пульт открывается в браузере **по умолчанию**, а не в том, которым управляет
|
Пульт открывается в браузере **по умолчанию**, а не в том, которым управляет
|
||||||
`deepseek-api`: иначе он занял бы там вкладку, и вкладка с чатом потерялась бы.
|
`deepseek-api`: иначе он занял бы там вкладку, и вкладка с чатом потерялась бы.
|
||||||
Порт 8787.
|
Порт 8787.
|
||||||
@@ -150,10 +163,23 @@ node C:\#Projects\deepwork\deepwork.mjs "почему падает загруз
|
|||||||
| `--files a.js,b.js` | положить эти файлы в вопрос сразу |
|
| `--files a.js,b.js` | положить эти файлы в вопрос сразу |
|
||||||
| `--steps 6` | сколько кругов позволить (по умолчанию 4) |
|
| `--steps 6` | сколько кругов позволить (по умолчанию 4) |
|
||||||
| `--apply` | записать пришедшие файлы на диск (без него только покажет) |
|
| `--apply` | записать пришедшие файлы на диск (без него только покажет) |
|
||||||
|
| `--no-tools` | не давать модели инструменты: один круг, только отмеченные файлы |
|
||||||
| `--chat search` | режим с поиском в интернете вместо обычного |
|
| `--chat search` | режим с поиском в интернете вместо обычного |
|
||||||
| `--check` | проверить связку и выйти |
|
| `--check` | проверить связку и выйти |
|
||||||
| `--selftest` | внутренние проверки, ничего не отправляя |
|
| `--selftest` | внутренние проверки, ничего не отправляя |
|
||||||
|
|
||||||
|
### Когда выключать инструменты
|
||||||
|
|
||||||
|
Инструменты хороши, когда **непонятно, что читать**: «почему падает загрузка»
|
||||||
|
в чужом проекте. Модель сама найдёт нужное.
|
||||||
|
|
||||||
|
Выключать стоит, когда вы **точно знаете нужные файлы**. Тогда:
|
||||||
|
|
||||||
|
- ответ приходит за один круг вместо двух-трёх;
|
||||||
|
- вопрос выходит вчетверо короче — приставка, дерево папки и напоминание
|
||||||
|
не едут вовсе (замер: 229 символов против 1886 на той же задаче);
|
||||||
|
- модель не ходит по папке без нужды, и наружу не уезжает лишний код.
|
||||||
|
|
||||||
### Чего ожидать по времени
|
### Чего ожидать по времени
|
||||||
|
|
||||||
Один круг — 7–15 секунд. Обычная задача укладывается в два-три круга, то есть
|
Один круг — 7–15 секунд. Обычная задача укладывается в два-три круга, то есть
|
||||||
@@ -374,6 +400,25 @@ DeepSeek видел, что данные неполные.
|
|||||||
живут в его `browser.mjs`. Отсюда остаётся одно требование — **не сворачивать
|
живут в его `browser.mjs`. Отсюда остаётся одно требование — **не сворачивать
|
||||||
окно браузера**.
|
окно браузера**.
|
||||||
|
|
||||||
|
### Блок-запрос вплотную к прозе, 26.08.2026
|
||||||
|
|
||||||
|
DeepSeek регулярно дописывает запрос в конец фразы, без перевода строки:
|
||||||
|
|
||||||
|
```
|
||||||
|
Мне нужны данные для ответа. Запрашиваю поиск константы MAGIC.=== FIND: MAGIC ===
|
||||||
|
```
|
||||||
|
|
||||||
|
Разбор требовал, чтобы блок начинал строку, и такой запрос не видел: цикл
|
||||||
|
решал, что запросов нет, и заканчивал работу догадкой. Круг впустую.
|
||||||
|
|
||||||
|
Якорь начала строки снят, якорь конца оставлен: упоминание формата посреди
|
||||||
|
фразы («верни `=== NEED: файл ===` и я пришлю») запросом по-прежнему не
|
||||||
|
считается. От запросов внутри блоков `=== FILE: ===` бережёт не якорь,
|
||||||
|
а вырезание этих блоков перед разбором — так было и раньше.
|
||||||
|
|
||||||
|
Проверено на живой задаче: до правки — один круг и «запросов нет»; после —
|
||||||
|
`FIND: MAGIC`, второй круг, верный ответ.
|
||||||
|
|
||||||
### Переезд на deepseek-api, 26.08.2026
|
### Переезд на deepseek-api, 26.08.2026
|
||||||
|
|
||||||
Раньше `deepwork` вызывал мост `ask.mjs` из проекта DeepShim дочерним процессом
|
Раньше `deepwork` вызывал мост `ask.mjs` из проекта DeepShim дочерним процессом
|
||||||
|
|||||||
+116
-14
@@ -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 rawArgs = process.argv.slice(2);
|
||||||
const positional = [];
|
const positional = [];
|
||||||
const flags = {};
|
const flags = {};
|
||||||
@@ -26,9 +33,11 @@ for (let i = 0; i < rawArgs.length; i++) {
|
|||||||
if (a.startsWith('--')) {
|
if (a.startsWith('--')) {
|
||||||
const eq = a.indexOf('=');
|
const eq = a.indexOf('=');
|
||||||
if (eq !== -1) { flags[a.slice(2, eq)] = a.slice(eq + 1); continue; }
|
if (eq !== -1) { flags[a.slice(2, eq)] = a.slice(eq + 1); continue; }
|
||||||
|
const name = a.slice(2);
|
||||||
const next = rawArgs[i + 1];
|
const next = rawArgs[i + 1];
|
||||||
if (next !== undefined && !next.startsWith('--')) { flags[a.slice(2)] = next; i++; }
|
if (!BOOL_FLAGS.has(name) && next !== undefined && !next.startsWith('--')) {
|
||||||
else flags[a.slice(2)] = true;
|
flags[name] = next; i++;
|
||||||
|
} else flags[name] = true;
|
||||||
} else positional.push(a);
|
} else positional.push(a);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +53,8 @@ deepwork — задать DeepSeek вопрос и дать ему дочита
|
|||||||
--steps N сколько кругов позволить (по умолчанию 4)
|
--steps N сколько кругов позволить (по умолчанию 4)
|
||||||
--apply применить пришедшие блоки === FILE: === на диск
|
--apply применить пришедшие блоки === FILE: === на диск
|
||||||
(прежняя версия уезжает в backup/)
|
(прежняя версия уезжает в backup/)
|
||||||
|
--no-tools не давать модели инструменты: один круг, только
|
||||||
|
отмеченные файлы. Так работает пульт «с галочками»
|
||||||
--chat роль think (по умолчанию) | search | vision
|
--chat роль think (по умолчанию) | search | vision
|
||||||
--timeout сек сколько секунд ждать ответа на один шаг
|
--timeout сек сколько секунд ждать ответа на один шаг
|
||||||
--check проверить, что сервис на месте и вкладка жива
|
--check проверить, что сервис на месте и вкладка жива
|
||||||
@@ -166,7 +177,14 @@ function parsePatches(answer) {
|
|||||||
function parseRequests(answer) {
|
function parseRequests(answer) {
|
||||||
const text = String(answer || '').replace(FILE_BLOCK_RE, '');
|
const text = String(answer || '').replace(FILE_BLOCK_RE, '');
|
||||||
const out = [];
|
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;
|
let m;
|
||||||
while ((m = re.exec(text))) out.push({ kind: m[1], arg: m[2] });
|
while ((m = re.exec(text))) out.push({ kind: m[1], arg: m[2] });
|
||||||
return out;
|
return out;
|
||||||
@@ -291,7 +309,14 @@ function isServerBusy(text) {
|
|||||||
|
|
||||||
// ------------------------------------------------------------------ цикл
|
// ------------------------------------------------------------------ цикл
|
||||||
|
|
||||||
async function buildFirst(root, files, question) {
|
// tools=false — режим «просто ответь по этим файлам»: ни приставки, ни дерева,
|
||||||
|
// ни напоминания про NEED/LIST/FIND. Тогда и круг ровно один. Инструменты
|
||||||
|
// хороши, когда неизвестно, что читать; когда файлы выбраны руками, они только
|
||||||
|
// тратят время и подбивают модель ходить по папке без нужды.
|
||||||
|
async function buildFirst(root, files, question, { tools = true, context = '' } = {}) {
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
if (tools) {
|
||||||
let head = '';
|
let head = '';
|
||||||
try {
|
try {
|
||||||
const raw = await readFile(join(HERE, 'agent-prompt.md'), 'utf8');
|
const raw = await readFile(join(HERE, 'agent-prompt.md'), 'utf8');
|
||||||
@@ -303,21 +328,30 @@ async function buildFirst(root, files, question) {
|
|||||||
// LIST **/* — два круга из четырёх ушли на выяснение, что вообще лежит в
|
// LIST **/* — два круга из четырёх ушли на выяснение, что вообще лежит в
|
||||||
// папке. Дерево этого проекта — 600 символов против двух минут ожидания.
|
// папке. Дерево этого проекта — 600 символов против двух минут ожидания.
|
||||||
const tree = (await listFiles(root)).slice(0, LIST_PATHS);
|
const tree = (await listFiles(root)).slice(0, LIST_PATHS);
|
||||||
const parts = [head, '',
|
parts.push(head, '',
|
||||||
'=== RESULT: LIST **/* ===',
|
'=== RESULT: LIST **/* ===',
|
||||||
fence(tree.map(f => `${f.path} ${f.size} б`).join('\n')),
|
fence(tree.map(f => `${f.path} ${f.size} б`).join('\n')),
|
||||||
'',
|
'');
|
||||||
];
|
}
|
||||||
|
|
||||||
for (const rel of files) {
|
for (const rel of files) {
|
||||||
const full = resolveInside(root, rel);
|
const full = resolveInside(root, rel);
|
||||||
if (!full) throw new Error(`файл вне рабочей папки: ${rel}`);
|
if (!full) throw new Error(`файл вне рабочей папки: ${rel}`);
|
||||||
parts.push(`=== FILE: ${rel} ===`, fence(await readFile(full, 'utf8')), FILE_CLOSE, '');
|
parts.push(`=== FILE: ${rel} ===`, fence(await readFile(full, 'utf8')), FILE_CLOSE, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Прошлый ответ, если человек попросил его сохранить. Метки те же, что
|
||||||
|
// у стенограммы кругов: формат один — модели проще.
|
||||||
|
if (String(context).trim()) {
|
||||||
|
parts.push('=== ТВОЙ ПРЕДЫДУЩИЙ ОТВЕТ ===', String(context).trim(), '=== END ===', '');
|
||||||
|
}
|
||||||
|
|
||||||
// Напоминание повторяется ВПЛОТНУЮ к вопросу. Первый живой прогон: приставка
|
// Напоминание повторяется ВПЛОТНУЮ к вопросу. Первый живой прогон: приставка
|
||||||
// ушла в начало сообщения, между ней и вопросом легли 22 КБ файла, и DeepSeek
|
// ушла в начало сообщения, между ней и вопросом легли 22 КБ файла, и DeepSeek
|
||||||
// инструменты проигнорировал — сам написал «точный код в другом файле» и
|
// инструменты проигнорировал — сам написал «точный код в другом файле» и
|
||||||
// выдал три догадки вместо запроса. Инструкция в хвосте весит больше.
|
// выдал три догадки вместо запроса. Инструкция в хвосте весит больше.
|
||||||
parts.push(TAIL_REMINDER, '', 'ВОПРОС:', question.trim());
|
if (tools) parts.push(TAIL_REMINDER, '');
|
||||||
|
parts.push('ВОПРОС:', question.trim());
|
||||||
return parts.join('\n');
|
return parts.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,13 +391,18 @@ function growTranscript(prev, answer, next) {
|
|||||||
next].join('\n');
|
next].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runAgent(cfg, root, question, files, log) {
|
async function runAgent(cfg, root, question, files, log, { tools = true, context = '' } = {}) {
|
||||||
let text = await buildFirst(root, files, question);
|
let text = await buildFirst(root, files, question, { tools, context });
|
||||||
let answer = '';
|
let answer = '';
|
||||||
let lastSig = '';
|
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 });
|
answer = await callDeepseek(cfg, text, { n, log });
|
||||||
|
if (!tools) break;
|
||||||
const requests = parseRequests(answer);
|
const requests = parseRequests(answer);
|
||||||
if (!requests.length) { log(`шаг ${n}: запросов нет, готово`); break; }
|
if (!requests.length) { log(`шаг ${n}: запросов нет, готово`); break; }
|
||||||
|
|
||||||
@@ -373,7 +412,7 @@ async function runAgent(cfg, root, question, files, log) {
|
|||||||
if (sig === lastSig) { log(`шаг ${n}: тот же запрос повторно — петля, выхожу`); break; }
|
if (sig === lastSig) { log(`шаг ${n}: тот же запрос повторно — петля, выхожу`); break; }
|
||||||
lastSig = sig;
|
lastSig = sig;
|
||||||
|
|
||||||
if (n === cfg.steps) { log(`шаг ${n}: шаги кончились, а запросы ещё идут`); break; }
|
if (n === steps) { log(`шаг ${n}: шаги кончились, а запросы ещё идут`); break; }
|
||||||
|
|
||||||
log(`шаг ${n}: запрошено ${requests.length}`);
|
log(`шаг ${n}: запрошено ${requests.length}`);
|
||||||
const result = await runRequests(root, requests, log);
|
const result = await runRequests(root, requests, log);
|
||||||
@@ -401,6 +440,54 @@ async function applyPatch(root, rel, content) {
|
|||||||
return { ok: true, backup };
|
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 секунды.
|
// Ход работы копится здесь, а страница забирает его опросом раз в 2 секунды.
|
||||||
@@ -451,6 +538,12 @@ async function serve(cfg) {
|
|||||||
} catch { return json(res, 200, { service: false, deepseek: false }); }
|
} 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')
|
if (req.method === 'GET' && url.pathname === '/api/progress')
|
||||||
return json(res, 200, { ...progress, seconds: progress.busy ? Math.round((Date.now() - progress.started) / 1000) : 0 });
|
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 {
|
try {
|
||||||
const run = { ...cfg, steps: Number.parseInt(body.steps, 10) || cfg.steps };
|
const run = { ...cfg, steps: Number.parseInt(body.steps, 10) || cfg.steps };
|
||||||
const answer = await runAgent(run, root, String(body.question), body.files || [],
|
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) });
|
return json(res, 200, { answer, steps: progress.steps, patches: parsePatches(answer) });
|
||||||
} finally { progress.busy = false; }
|
} finally { progress.busy = false; }
|
||||||
}
|
}
|
||||||
@@ -523,6 +617,13 @@ async function selftest() {
|
|||||||
|
|
||||||
const req = parseRequests(sample);
|
const req = parseRequests(sample);
|
||||||
assert.deepEqual(req.map(r => r.kind), ['NEED', 'LIST', 'FIND']);
|
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');
|
assert.equal(req[0].arg, 'ask.mjs, ui.html');
|
||||||
|
|
||||||
const pat = parsePatches(sample);
|
const pat = parsePatches(sample);
|
||||||
@@ -582,7 +683,8 @@ if (flags.ui || positional[0] === 'ui') {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`папка: ${root}`);
|
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('\n----- ответ DeepSeek -----');
|
||||||
console.log(answer);
|
console.log(answer);
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,10 @@
|
|||||||
<div class="row"><input type="text" id="filter" placeholder="фильтр по имени"></div>
|
<div class="row"><input type="text" id="filter" placeholder="фильтр по имени"></div>
|
||||||
<div class="scroll" id="tree">загружаю…</div>
|
<div class="scroll" id="tree">загружаю…</div>
|
||||||
<div class="row"><button id="none">Снять все</button><span class="muted" id="sel">ничего не выбрано</span></div>
|
<div class="row"><button id="none">Снять все</button><span class="muted" id="sel">ничего не выбрано</span></div>
|
||||||
|
<div class="row" id="gitrow" hidden>
|
||||||
|
<button id="gitinit">Инициализировать git</button>
|
||||||
|
<span class="muted" id="gitnote"></span>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
@@ -47,8 +51,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button class="primary" id="send">Спросить</button>
|
<button class="primary" id="send">Спросить</button>
|
||||||
<label class="muted">шагов <select id="steps" style="width:auto">
|
<label class="muted" title="Разрешить модели самой запрашивать файлы блоками NEED / LIST / FIND. Выключено — один круг и только отмеченные файлы.">
|
||||||
|
<input type="checkbox" id="tools" checked> инструменты</label>
|
||||||
|
<label class="muted" id="stepsbox">шагов <select id="steps" style="width:auto">
|
||||||
<option>2</option><option selected>4</option><option>6</option><option>8</option></select></label>
|
<option>2</option><option selected>4</option><option>6</option><option>8</option></select></label>
|
||||||
|
<label class="muted" title="Прошлый ответ уедет вместе со следующим вопросом.">
|
||||||
|
<input type="checkbox" id="keep"> помнить прошлый ответ</label>
|
||||||
<span class="muted" id="status"></span>
|
<span class="muted" id="status"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="scroll" id="out"></div>
|
<div class="scroll" id="out"></div>
|
||||||
@@ -56,6 +64,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
let files = [];
|
let files = [];
|
||||||
|
let lastAnswer = ''; // для галочки «помнить прошлый ответ»
|
||||||
|
|
||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
const esc = s => s.replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
const esc = s => s.replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
||||||
@@ -79,6 +88,12 @@ function count() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$('tree').addEventListener('change', count);
|
$('tree').addEventListener('change', count);
|
||||||
|
|
||||||
|
// Без инструментов круг ровно один — выбор числа шагов теряет смысл.
|
||||||
|
$('tools').addEventListener('change', () => {
|
||||||
|
$('stepsbox').style.opacity = $('tools').checked ? '' : '.4';
|
||||||
|
$('steps').disabled = !$('tools').checked;
|
||||||
|
});
|
||||||
$('filter').addEventListener('input', renderTree);
|
$('filter').addEventListener('input', renderTree);
|
||||||
$('none').onclick = () => { document.querySelectorAll('#tree input').forEach(i => i.checked = false); count(); };
|
$('none').onclick = () => { document.querySelectorAll('#tree input').forEach(i => i.checked = false); count(); };
|
||||||
|
|
||||||
@@ -86,6 +101,32 @@ fetch('/api/tree').then(r => r.json()).then(d => {
|
|||||||
files = d.files; $('root').textContent = d.root; renderTree();
|
files = d.files; $('root').textContent = d.root; renderTree();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ряд с git всплывает, только когда он нужен: репозитория нет или в .gitignore
|
||||||
|
// не хватает строк. Главная из них — backup/: туда ложатся прежние версии
|
||||||
|
// файлов перед записью, и однажды они уедут в коммит.
|
||||||
|
async function gitState() {
|
||||||
|
try {
|
||||||
|
const g = await fetch('/api/git').then(r => r.json());
|
||||||
|
const need = !g.repo || g.missing.length;
|
||||||
|
$('gitrow').hidden = !need;
|
||||||
|
if (!need) return;
|
||||||
|
$('gitinit').textContent = g.repo ? 'Дополнить .gitignore' : 'Инициализировать git';
|
||||||
|
$('gitnote').textContent = g.repo ? `не хватает: ${g.missing.join(', ')}`
|
||||||
|
: 'папка не под git';
|
||||||
|
} catch { $('gitrow').hidden = true; }
|
||||||
|
}
|
||||||
|
gitState();
|
||||||
|
|
||||||
|
$('gitinit').onclick = async () => {
|
||||||
|
$('gitinit').disabled = true;
|
||||||
|
const r = await fetch('/api/git/init', { method: 'POST' }).then(x => x.json());
|
||||||
|
$('gitnote').textContent = r.ok
|
||||||
|
? (r.added.length ? `добавлено в .gitignore: ${r.added.join(', ')}` : 'всё уже на месте')
|
||||||
|
: `не вышло: ${r.reason}`;
|
||||||
|
$('gitinit').disabled = false;
|
||||||
|
if (r.ok) setTimeout(gitState, 1200);
|
||||||
|
};
|
||||||
|
|
||||||
async function status() {
|
async function status() {
|
||||||
try {
|
try {
|
||||||
const s = await fetch('/api/status').then(r => r.json());
|
const s = await fetch('/api/status').then(r => r.json());
|
||||||
@@ -118,7 +159,13 @@ $('send').onclick = async () => {
|
|||||||
try {
|
try {
|
||||||
const r = await fetch('/api/ask', {
|
const r = await fetch('/api/ask', {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ question, files: chosen(), steps: +$('steps').value }),
|
body: JSON.stringify({
|
||||||
|
question, files: chosen(), steps: +$('steps').value,
|
||||||
|
tools: $('tools').checked,
|
||||||
|
// Галочку между вопросами не сбрасываем: если человек ведёт разговор,
|
||||||
|
// он ведёт его несколькими вопросами подряд.
|
||||||
|
context: $('keep').checked ? lastAnswer : '',
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
clearInterval(poll);
|
clearInterval(poll);
|
||||||
@@ -131,6 +178,7 @@ $('send').onclick = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function render(d) {
|
function render(d) {
|
||||||
|
lastAnswer = d.answer || '';
|
||||||
const steps = d.steps.map(s => `<div class="step">${esc(s)}</div>`).join('');
|
const steps = d.steps.map(s => `<div class="step">${esc(s)}</div>`).join('');
|
||||||
const patches = d.patches.map((p, i) => `
|
const patches = d.patches.map((p, i) => `
|
||||||
<div class="patch">
|
<div class="patch">
|
||||||
|
|||||||
Reference in New Issue
Block a user