Первая версия: инструменты и цикл для DeepSeek
DeepSeek через веб-чат видит только то, что человек положил в вопрос. Здесь у него появляется обратное направление: он возвращает блок-запрос, программа исполняет его и досылает результат следующим сообщением в тот же чат. Обычный агентский цикл, только транспорт — текст. Инструменты (только чтение, только внутри рабочей папки): === NEED: путь, путь === прислать файлы целиком === LIST: **/*.mjs === список файлов по маске === FIND: текст === где встречается, с номерами строк Запуска команд намеренно нет: исполнять строки из ответа, пришедшего из интернета, без белого списка и подтверждения нельзя. Мост ollama_orchestro/ask.mjs не импортируется и не копируется — он разбирает process.argv и исполняет команду на верхнем уровне, поэтому import немедленно запустил бы её. Вызывается подпроцессом теми флагами, которые у него уже есть: шаг 1 без --new (правка первого сообщения), шаги 2..N с --new, иначе DeepSeek не помнит предыдущий шаг. Взамен orchestro не меняется ни на строку, а его правки прилетают сюда даром. Два урока, каждый стоил живого прогона и вписан в код: - инструкция в начале сообщения не работает. 22 КБ файла между приставкой и вопросом — и блоки-запросы игнорируются. Помогает дословное напоминание вплотную к вопросу; - дерево папки надо давать сразу. Без него DeepSeek угадывает имена файлов и тратит два круга по минуте на LIST. Есть веб-пульт (start-deepwork.cmd, бросить папку): файлы галочками, журнал шагов по ходу, запись правок с копией в backup/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>deepwork</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; height: 100vh; display: grid; grid-template-columns: 320px 1fr;
|
||||
font: 14px/1.45 system-ui, "Segoe UI", sans-serif; background: #14161a; color: #e6e8ec; }
|
||||
aside { border-right: 1px solid #262a31; display: flex; flex-direction: column; min-height: 0; }
|
||||
main { display: flex; flex-direction: column; min-height: 0; }
|
||||
h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .06em; color: #8b93a1;
|
||||
margin: 0; padding: 12px; border-bottom: 1px solid #262a31; }
|
||||
.scroll { overflow: auto; flex: 1; padding: 8px 12px; min-height: 0; }
|
||||
label.f { display: flex; gap: 8px; align-items: baseline; padding: 2px 0; cursor: pointer; }
|
||||
label.f span { color: #6c7381; font-size: 12px; margin-left: auto; }
|
||||
.row { display: flex; gap: 8px; align-items: center; padding: 10px 12px; border-top: 1px solid #262a31; }
|
||||
.muted { color: #8b93a1; font-size: 12px; }
|
||||
input[type=text], textarea, select { background: #1b1e24; color: #e6e8ec; border: 1px solid #333844;
|
||||
border-radius: 6px; padding: 8px; font: inherit; width: 100%; }
|
||||
textarea { resize: vertical; min-height: 84px; }
|
||||
button { background: #262b34; color: #e6e8ec; border: 1px solid #3a414f; border-radius: 6px;
|
||||
padding: 7px 14px; font: inherit; cursor: pointer; }
|
||||
button:hover:not(:disabled) { background: #2f3540; }
|
||||
button.primary { background: #3b6ea5; border-color: #4a83c0; }
|
||||
button:disabled { opacity: .5; cursor: default; }
|
||||
pre { background: #1b1e24; border: 1px solid #262a31; border-radius: 6px; padding: 10px;
|
||||
white-space: pre-wrap; word-break: break-word; margin: 0 0 12px; }
|
||||
.step { font-family: ui-monospace, Consolas, monospace; font-size: 12px; color: #9fd08b; }
|
||||
.dot { width: 8px; height: 8px; border-radius: 50%; background: #7a4; display: inline-block; }
|
||||
.dot.bad { background: #c55; }
|
||||
.patch { border: 1px solid #333844; border-radius: 6px; margin-bottom: 10px; }
|
||||
.patch > .row { border-top: none; }
|
||||
.patch pre { margin: 0 12px 12px; max-height: 300px; overflow: auto; }
|
||||
</style>
|
||||
|
||||
<aside>
|
||||
<h2>Файлы <span class="muted" id="root"></span></h2>
|
||||
<div class="row"><input type="text" id="filter" placeholder="фильтр по имени"></div>
|
||||
<div class="scroll" id="tree">загружаю…</div>
|
||||
<div class="row"><button id="none">Снять все</button><span class="muted" id="sel">ничего не выбрано</span></div>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<h2><span class="dot" id="dot"></span> <span id="bar">проверяю мост…</span></h2>
|
||||
<div style="padding:12px">
|
||||
<textarea id="q" placeholder="Например: почему ответ приходит обрезанным? Дай исправленную функцию целиком."></textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="primary" id="send">Спросить</button>
|
||||
<label class="muted">шагов <select id="steps" style="width:auto">
|
||||
<option>2</option><option selected>4</option><option>6</option><option>8</option></select></label>
|
||||
<span class="muted" id="status"></span>
|
||||
</div>
|
||||
<div class="scroll" id="out"></div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
let files = [];
|
||||
|
||||
const $ = id => document.getElementById(id);
|
||||
const esc = s => s.replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
||||
const chosen = () => [...document.querySelectorAll('#tree input:checked')].map(i => i.value);
|
||||
|
||||
function renderTree() {
|
||||
const needle = $('filter').value.toLowerCase();
|
||||
const keep = new Set(chosen());
|
||||
$('tree').innerHTML = files
|
||||
.filter(f => !needle || f.path.toLowerCase().includes(needle))
|
||||
.map(f => `<label class="f"><input type="checkbox" value="${esc(f.path)}"${keep.has(f.path) ? ' checked' : ''}>
|
||||
${esc(f.path)}<span>${Math.max(1, Math.round(f.size / 1024))} КБ</span></label>`)
|
||||
.join('') || '<div class="muted">ничего не подошло</div>';
|
||||
count();
|
||||
}
|
||||
|
||||
function count() {
|
||||
const c = chosen();
|
||||
const kb = files.filter(f => c.includes(f.path)).reduce((s, f) => s + f.size, 0) / 1024;
|
||||
$('sel').textContent = c.length ? `${c.length} шт., ${Math.round(kb)} КБ` : 'ничего не выбрано';
|
||||
}
|
||||
|
||||
$('tree').addEventListener('change', count);
|
||||
$('filter').addEventListener('input', renderTree);
|
||||
$('none').onclick = () => { document.querySelectorAll('#tree input').forEach(i => i.checked = false); count(); };
|
||||
|
||||
fetch('/api/tree').then(r => r.json()).then(d => {
|
||||
files = d.files; $('root').textContent = d.root; renderTree();
|
||||
});
|
||||
|
||||
async function status() {
|
||||
try {
|
||||
const s = await fetch('/api/status').then(r => r.json());
|
||||
$('dot').className = 'dot' + (s.deepseek ? '' : ' bad');
|
||||
$('bar').textContent = s.deepseek ? 'мост на месте, вкладка DeepSeek открыта'
|
||||
: s.browser ? 'браузер есть, но вкладки DeepSeek нет' : 'браузер с портом 9333 не найден — запустите start-browser.cmd';
|
||||
} catch { $('dot').className = 'dot bad'; $('bar').textContent = 'пульт не отвечает'; }
|
||||
}
|
||||
status(); setInterval(status, 5000);
|
||||
|
||||
// Пока идёт прогон, страница не должна молчать: круг занимает до 4 минут,
|
||||
// и без счётчика непонятно, работает ли что-то вообще.
|
||||
let poll = null;
|
||||
function watch() {
|
||||
poll = setInterval(async () => {
|
||||
const p = await fetch('/api/progress').then(r => r.json()).catch(() => null);
|
||||
if (!p) return;
|
||||
$('status').textContent = `идёт ${p.seconds} с`;
|
||||
$('out').innerHTML = '<pre>' + p.steps.map(s => `<div class="step">${esc(s)}</div>`).join('') + '</pre>';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
$('send').onclick = async () => {
|
||||
const question = $('q').value.trim();
|
||||
if (!question) return;
|
||||
$('send').disabled = true;
|
||||
$('out').innerHTML = '<pre class="muted">отправляю…</pre>';
|
||||
watch();
|
||||
try {
|
||||
const r = await fetch('/api/ask', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ question, files: chosen(), steps: +$('steps').value }),
|
||||
});
|
||||
const d = await r.json();
|
||||
clearInterval(poll);
|
||||
if (d.error) { $('out').innerHTML = `<pre style="color:#e88">${esc(d.error)}</pre>`; return; }
|
||||
render(d);
|
||||
} catch (e) {
|
||||
clearInterval(poll);
|
||||
$('out').innerHTML = `<pre style="color:#e88">${esc(String(e))}</pre>`;
|
||||
} finally { $('send').disabled = false; $('status').textContent = ''; }
|
||||
};
|
||||
|
||||
function render(d) {
|
||||
const steps = d.steps.map(s => `<div class="step">${esc(s)}</div>`).join('');
|
||||
const patches = d.patches.map((p, i) => `
|
||||
<div class="patch">
|
||||
<div class="row"><b>${esc(p.path)}</b>
|
||||
<span class="muted">${p.content.split('\n').length} строк</span>
|
||||
<button data-i="${i}" style="margin-left:auto">Записать</button>
|
||||
<span class="muted" id="note${i}"></span></div>
|
||||
<pre>${esc(p.content.slice(0, 4000))}${p.content.length > 4000 ? '\n…' : ''}</pre>
|
||||
</div>`).join('');
|
||||
|
||||
$('out').innerHTML = `<pre>${steps}</pre>
|
||||
<h2 style="border:none;padding:0 0 8px">Ответ</h2>
|
||||
<pre>${esc(d.answer)}</pre>
|
||||
${d.patches.length ? `<h2 style="border:none;padding:0 0 8px">Файлы в ответе</h2>${patches}` : ''}`;
|
||||
|
||||
$('out').querySelectorAll('.patch button').forEach(b => b.onclick = async () => {
|
||||
const p = d.patches[+b.dataset.i];
|
||||
b.disabled = true;
|
||||
const r = await fetch('/api/apply', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: p.path, content: p.content }),
|
||||
}).then(x => x.json());
|
||||
document.getElementById('note' + b.dataset.i).textContent =
|
||||
r.ok ? (r.backup ? `записан, копия: ${r.backup}` : 'записан (новый файл)') : `не записан: ${r.reason}`;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user