DeepSeek через deepseek-api; память шагов переехала из чата в стенограмму
Транспорт: был дочерний процесс ask.mjs из ollama_orchestro с обменом через временные файлы, стал POST /api/chat к сервису deepseek-api. Он держит вкладку браузера и отдаёт наружу протокол Ollama, поэтому здесь про браузер больше не знают: ушли проверка существования моста, временные файлы шагов вместе с флагом --keep-temp и четыре осиротевших импорта. Главное не в транспорте. Раньше шаги 2..N уходили с флагом --new, и ход разговора помнил сам чат DeepSeek. Сервис памяти не имеет принципиально: каждый запрос он кладёт в чат целиком, затирая прошлый. Поэтому стенограмму ведём здесь — growTranscript подклеивает к отправленному тексту прошлый ответ модели между метками === ТВОЙ ПРЕДЫДУЩИЙ ОТВЕТ === и === END ===, и следующим шагом уезжает всё сразу. Так надёжнее, чем было: видно в одном месте, что именно уедет модели, а чужой потребитель, вклинившийся между нашими шагами, ничего не портит — мы всё приносим заново. Плата — вопрос растёт на длину прошлого ответа. --check теперь различает три состояния: сервис отвечает и браузер поднят; сервис жив, но браузера ещё нет; сервис не запущен. Плашка в ui.html тоже. Настройки: bridge/chat/cdpPort заменены на deepseekHost и deepseekModel. Проверено живым многошаговым прогоном: шаг 1 — 2094 символа, ответ NEED: deepwork.mjs; шаг 2 — 28 447 символов стенограммы, ответ дословно верный и по вопросу из ПЕРВОГО сообщения, то есть память шагов работает. Два шага за 17 с. --selftest пройден, --check зелёный. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3586e32a89
commit
44f07c8af9
@@ -1,6 +1,6 @@
|
|||||||
# deepwork
|
# deepwork
|
||||||
|
|
||||||
Цикл с инструментами поверх моста `ollama_orchestro/ask.mjs`.
|
Цикл с инструментами поверх сервиса [deepseek-api](https://git.08h.ru/alex/deepseek-api).
|
||||||
|
|
||||||
DeepSeek через веб-чат видит только то, что положили ему в вопрос. Здесь у него
|
DeepSeek через веб-чат видит только то, что положили ему в вопрос. Здесь у него
|
||||||
появляется обратное направление: он возвращает блок-запрос, программа его
|
появляется обратное направление: он возвращает блок-запрос, программа его
|
||||||
@@ -45,23 +45,21 @@ node c:\#Projects\deepwork\deepwork.mjs ui тот же пульт вру
|
|||||||
|
|
||||||
## Как устроено
|
## Как устроено
|
||||||
|
|
||||||
Мост **не импортируется и не копируется**. `ask.mjs` разбирает `process.argv`
|
У веб-версии DeepSeek нет API — только вкладка в браузере. Разговором с ней
|
||||||
и исполняет команду на верхнем уровне — `import` немедленно запустил бы её.
|
занимается отдельный сервис `deepseek-api`, а нам показывает протокол Ollama.
|
||||||
Поэтому мост вызывается подпроцессом теми флагами, которые у него уже есть:
|
Здесь про браузер не знают вовсе: обычный `POST /api/chat`.
|
||||||
|
|
||||||
```
|
**Разговор помним мы, а не DeepSeek.** Сервис без памяти: каждый запрос он
|
||||||
node ask.mjs ask --chat think --file step1.txt --out ans1.txt --quiet
|
кладёт в чат целиком, затирая прошлый. Поэтому стенограмма растёт здесь —
|
||||||
node ask.mjs ask --chat think --new --file step2.txt --out ans2.txt --quiet
|
`growTranscript` подклеивает к отправленному тексту прошлый ответ модели
|
||||||
```
|
(между метками `=== ТВОЙ ПРЕДЫДУЩИЙ ОТВЕТ ===` и `=== END ===`) и результаты
|
||||||
|
запросов, и следующим шагом уезжает всё сразу.
|
||||||
|
|
||||||
Шаг 1 идёт без `--new`: мост правит первое сообщение, и чат остаётся из одного
|
Раньше эту роль играл флаг `--new` у моста, и помнил всё сам чат. Своя
|
||||||
обмена. Шаги 2..N — с `--new`, обычной отправкой: только так DeepSeek помнит
|
стенограмма надёжнее: видно в одном месте, что именно уедет модели, а чужой
|
||||||
предыдущий шаг. Цена — переподключение к браузеру на каждом шаге, 1–2 секунды.
|
потребитель, вклинившийся между нашими шагами, ничего не портит — мы всё
|
||||||
Взамен `ollama_orchestro` не меняется ни на строку, а его правки прилетают сюда
|
приносим заново. Плата — вопрос растёт на длину прошлого ответа; при `steps: 4`
|
||||||
даром.
|
это единицы килобайт против 22 КБ первого сообщения.
|
||||||
|
|
||||||
Чат берётся тот же, что у orchestro (`think` из его `chats.json`). Своя роль не
|
|
||||||
заводится: `chats.json` принадлежит мосту.
|
|
||||||
|
|
||||||
Цикл останавливается, когда запросов в ответе нет, когда кончились шаги или
|
Цикл останавливается, когда запросов в ответе нет, когда кончились шаги или
|
||||||
когда пришёл тот же самый запрос второй раз подряд (петля).
|
когда пришёл тот же самый запрос второй раз подряд (петля).
|
||||||
@@ -71,20 +69,35 @@ node ask.mjs ask --chat think --new --file step2.txt --out ans2.txt --quiet
|
|||||||
`config.json` (в репозиторий не попадает, образец — `config.example.json`):
|
`config.json` (в репозиторий не попадает, образец — `config.example.json`):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "bridge": "C:/#Projects/ollama_orchestro/ask.mjs", "chat": "think", "steps": 4 }
|
{ "deepseekHost": "http://localhost:11500", "deepseekModel": "deepseek:think", "steps": 4 }
|
||||||
```
|
```
|
||||||
|
|
||||||
Без файла мост ищется рядом: `../ollama_orchestro/ask.mjs`.
|
Без файла берётся `http://localhost:11500`.
|
||||||
|
|
||||||
## Проверка
|
## Проверка
|
||||||
|
|
||||||
```
|
```
|
||||||
node deepwork.mjs --selftest внутренние проверки, ничего не отправляют
|
node deepwork.mjs --selftest внутренние проверки, ничего не отправляют
|
||||||
node deepwork.mjs --check мост на месте, вкладка жива
|
node deepwork.mjs --check сервис на месте, вкладка жива
|
||||||
```
|
```
|
||||||
|
|
||||||
Браузер должен быть запущен `start-browser.cmd` из `ollama_orchestro` —
|
`--check` различает три состояния: сервис отвечает и браузер поднят; сервис
|
||||||
с отладочным портом 9333.
|
жив, но браузера ещё нет (встанет на первом же вопросе); сервис не запущен.
|
||||||
|
|
||||||
|
Запускается сервис своим `start.cmd` в папке `deepseek-api` — браузер он
|
||||||
|
поднимает сам.
|
||||||
|
|
||||||
|
## Переезд на deepseek-api (26.08.2026)
|
||||||
|
|
||||||
|
Транспорт сменился: был дочерний процесс `ask.mjs` из `ollama_orchestro`
|
||||||
|
с обменом через временные файлы, стал `POST /api/chat` к сервису
|
||||||
|
`deepseek-api`. Заодно ушли временные файлы шагов (и флаг `--keep-temp`),
|
||||||
|
проверка существования моста и запуск браузера — всё это теперь не наша забота.
|
||||||
|
|
||||||
|
Память шагов переехала из чата DeepSeek в `growTranscript` (см. «Как устроено»).
|
||||||
|
Проверено живым прогоном: шаг 1 — 2094 символа, ответ `NEED: deepwork.mjs`;
|
||||||
|
шаг 2 — 28 447 символов стенограммы, ответ дословно верный и по вопросу
|
||||||
|
из ПЕРВОГО сообщения. Два шага за 17 с.
|
||||||
|
|
||||||
## Что проверено
|
## Что проверено
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"bridge": "C:\\#Projects\\ollama_orchestro\\ask.mjs",
|
"deepseekHost": "http://localhost:11500",
|
||||||
"chat": "think",
|
"deepseekModel": "deepseek:think",
|
||||||
"steps": 4,
|
"steps": 4,
|
||||||
"timeout": 240
|
"timeout": 240
|
||||||
}
|
}
|
||||||
|
|||||||
+87
-64
@@ -1,22 +1,18 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// deepwork — цикл с инструментами поверх моста ollama_orchestro/ask.mjs.
|
// deepwork — цикл с инструментами поверх сервиса deepseek-api.
|
||||||
//
|
//
|
||||||
// DeepSeek через веб-чат видит только то, что ему положили в вопрос. Здесь он
|
// DeepSeek через веб-чат видит только то, что ему положили в вопрос. Здесь он
|
||||||
// получает обратное направление: возвращает блок-запрос (NEED/LIST/FIND),
|
// получает обратное направление: возвращает блок-запрос (NEED/LIST/FIND),
|
||||||
// программа его исполняет и досылает результат следующим сообщением в тот же
|
// программа его исполняет и досылает результат следующим сообщением в тот же
|
||||||
// чат. Получается обычный агентский цикл, только транспорт — текст.
|
// чат. Получается обычный агентский цикл, только транспорт — текст.
|
||||||
//
|
//
|
||||||
// Мост НЕ импортируется и НЕ копируется: ask.mjs разбирает process.argv и
|
// С DeepSeek говорим по HTTP: у веб-версии API нет, но рядом крутится сервис
|
||||||
// исполняет команду на верхнем уровне, поэтому import немедленно запустил бы
|
// deepseek-api, который держит вкладку браузера и отдаёт наружу протокол
|
||||||
// её. Вызываем его подпроцессом теми флагами, которые у него уже есть.
|
// Ollama. Здесь про браузер не знают вовсе.
|
||||||
// ollama_orchestro при этом не меняется ни на строку.
|
|
||||||
|
|
||||||
import { readFile, writeFile, mkdir, copyFile, readdir, stat, rm } from 'node:fs/promises';
|
import { readFile, writeFile, mkdir, copyFile, readdir, stat } from 'node:fs/promises';
|
||||||
import { existsSync } from 'node:fs';
|
|
||||||
import { spawn } from 'node:child_process';
|
|
||||||
import { resolve, join, dirname, relative, isAbsolute } from 'node:path';
|
import { resolve, join, dirname, relative, isAbsolute } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { tmpdir } from 'node:os';
|
|
||||||
|
|
||||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
@@ -50,7 +46,6 @@ deepwork — задать DeepSeek вопрос и дать ему дочита
|
|||||||
(прежняя версия уезжает в backup/)
|
(прежняя версия уезжает в backup/)
|
||||||
--chat роль think (по умолчанию) | search
|
--chat роль think (по умолчанию) | search
|
||||||
--timeout сек максимум бездействия страницы, передаётся мосту
|
--timeout сек максимум бездействия страницы, передаётся мосту
|
||||||
--keep-temp не удалять временные файлы шагов
|
|
||||||
--check проверить, что мост на месте и вкладка жива
|
--check проверить, что мост на месте и вкладка жива
|
||||||
--selftest прогнать внутренние проверки, ничего не отправляя
|
--selftest прогнать внутренние проверки, ничего не отправляя
|
||||||
|
|
||||||
@@ -61,18 +56,21 @@ deepwork — задать DeepSeek вопрос и дать ему дочита
|
|||||||
=== LIST: **/*.mjs === список файлов по маске
|
=== LIST: **/*.mjs === список файлов по маске
|
||||||
=== FIND: текст === где встречается текст
|
=== FIND: текст === где встречается текст
|
||||||
|
|
||||||
Мост берётся из config.json (ключ bridge), по умолчанию —
|
Адрес сервиса берётся из config.json (ключ deepseekHost), по умолчанию —
|
||||||
../ollama_orchestro/ask.mjs рядом с этой папкой.
|
http://localhost:11500. Сервис запускается своим start.cmd.
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
// ------------------------------------------------------------------ настройки
|
// ------------------------------------------------------------------ настройки
|
||||||
|
|
||||||
const CONFIG_DEFAULTS = {
|
const CONFIG_DEFAULTS = {
|
||||||
bridge: resolve(HERE, '..', 'ollama_orchestro', 'ask.mjs'),
|
// У веб-версии DeepSeek нет API. Рядом крутится отдельный сервис deepseek-api
|
||||||
chat: 'think',
|
// (git.08h.ru/alex/deepseek-api): он держит вкладку браузера и отдаёт наружу
|
||||||
|
// протокол Ollama. Запускается своим start.cmd, браузер поднимает сам.
|
||||||
|
deepseekHost: 'http://localhost:11500',
|
||||||
|
deepseekModel: 'deepseek:think', // think | search | vision — режим чата
|
||||||
steps: 4,
|
steps: 4,
|
||||||
timeout: 240,
|
timeout: 240, // секунд на один шаг; сверх этого сервису дают ещё две минуты
|
||||||
cdpPort: 9333, // отладочный порт браузера — только чтобы показать, жив ли мост
|
// на очередь: вкладка одна на всех потребителей
|
||||||
uiPort: 8787, // не 8777: там живёт пульт orchestro, оба могут работать разом
|
uiPort: 8787, // не 8777: там живёт пульт orchestro, оба могут работать разом
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -238,42 +236,50 @@ async function runRequests(root, requests, log) {
|
|||||||
return parts.filter(Boolean).join('\n\n');
|
return parts.filter(Boolean).join('\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ мост
|
// ------------------------------------------------------------------ DeepSeek
|
||||||
|
|
||||||
// Один шаг: текст уходит через --file, ответ забирается через --out. Оба пути
|
// Один шаг: вопрос уходит по HTTP, ответ приходит там же. Раньше здесь был
|
||||||
// абсолютные, поэтому рабочая папка подпроцесса роли не играет.
|
// дочерний процесс с обменом через временные файлы — разговор с вкладкой
|
||||||
// fresh=true — первое сообщение (мост правит его и чат остаётся из одного
|
// браузера уехал в deepseek-api, а нам он показывает протокол Ollama.
|
||||||
// обмена); fresh=false — обычная отправка, только так DeepSeek помнит шаг.
|
async function callDeepseek(cfg, text, { n, log }) {
|
||||||
async function callBridge(cfg, text, { fresh, temp, n, log }) {
|
log(`шаг ${n}: ${text.length} символов, спрашиваю DeepSeek…`);
|
||||||
const qFile = join(temp, `step${n}-q.txt`);
|
|
||||||
const aFile = join(temp, `step${n}-a.txt`);
|
|
||||||
await writeFile(qFile, text, 'utf8');
|
|
||||||
|
|
||||||
const args = ['ask', '--chat', String(cfg.chat), '--file', qFile, '--out', aFile,
|
|
||||||
'--timeout', String(cfg.timeout), '--quiet'];
|
|
||||||
if (!fresh) args.push('--new');
|
|
||||||
|
|
||||||
log(`шаг ${n}: ${text.length} символов${fresh ? '' : ' (--new)'}, жду мост…`);
|
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
const code = await new Promise((done, fail) => {
|
|
||||||
const p = spawn(process.execPath, [cfg.bridge, ...args], { cwd: dirname(cfg.bridge) });
|
|
||||||
let err = '';
|
|
||||||
p.stdout.on('data', () => {});
|
|
||||||
p.stderr.on('data', d => { err += d.toString(); });
|
|
||||||
p.on('error', fail);
|
|
||||||
p.on('close', c => { if (c !== 0) fail(new Error(err.trim() || `мост вышел с кодом ${c}`)); else done(c); });
|
|
||||||
});
|
|
||||||
void code;
|
|
||||||
|
|
||||||
const answer = await readFile(aFile, 'utf8');
|
let r;
|
||||||
|
try {
|
||||||
|
r = await fetch(`${cfg.deepseekHost}/api/chat`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: cfg.deepseekModel,
|
||||||
|
stream: false, // иначе придёт NDJSON, а нам нужен объект
|
||||||
|
messages: [{ role: 'user', content: text }],
|
||||||
|
}),
|
||||||
|
// Свой срок больше сервисного: там таймаут считает БЕЗДЕЙСТВИЕ страницы,
|
||||||
|
// а сюда добавляется ещё и очередь — вкладка одна на всех потребителей.
|
||||||
|
signal: AbortSignal.timeout((cfg.timeout + 120) * 1000),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
`deepseek-api не отвечает по адресу ${cfg.deepseekHost}: ${e.message}\n` +
|
||||||
|
'Запустите start.cmd в папке deepseek-api — браузер он поднимет сам.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await r.json().catch(() => ({}));
|
||||||
|
if (!r.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`deepseek-api ответил ${r.status}: ${data.error || 'без пояснений'}\n` +
|
||||||
|
`Что видно во вкладке: curl ${cfg.deepseekHost}/api/probe`);
|
||||||
|
}
|
||||||
|
const answer = data.message?.content ?? '';
|
||||||
log(` ответ ${answer.length} символов за ${Math.round((Date.now() - started) / 1000)} с`);
|
log(` ответ ${answer.length} символов за ${Math.round((Date.now() - started) / 1000)} с`);
|
||||||
|
|
||||||
// Мост сам жмёт Retry, когда DeepSeek отвечает «Server is busy», но попытки
|
// Сервис сам жмёт Retry, когда DeepSeek отвечает «Server is busy», но попытки
|
||||||
// там не бесконечны. Если такой текст всё-таки доехал — это не ответ, и
|
// там не бесконечны. Если такой текст всё-таки доехал — это не ответ, и
|
||||||
// разбирать его как ответ нельзя: в блоках-запросах и правках его нет,
|
// разбирать его как ответ нельзя: в блоках-запросах и правках его нет,
|
||||||
// а цикл решил бы, что запросов не поступило, и молча закончил работу.
|
// а цикл решил бы, что запросов не поступило, и молча закончил работу.
|
||||||
if (isServerBusy(answer)) throw new Error(
|
if (isServerBusy(answer)) throw new Error(
|
||||||
'DeepSeek занят: «Server is busy». Мост исчерпал повторы — попробуйте позже.');
|
'DeepSeek занят: «Server is busy». Повторы исчерпаны — попробуйте позже.');
|
||||||
return answer;
|
return answer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,16 +343,27 @@ const FOLLOW_HEAD = [
|
|||||||
'',
|
'',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
async function runAgent(cfg, root, question, files, log) {
|
// Ход разговора ведём здесь, а не в чате DeepSeek. Сервис памяти не имеет:
|
||||||
const temp = join(tmpdir(), `deepwork-${process.pid}`);
|
// каждый запрос он кладёт в чат целиком, затирая прошлый. Раньше эту роль играл
|
||||||
await mkdir(temp, { recursive: true });
|
// флаг --new, и помнил всё сам DeepSeek; теперь стенограмма растёт у нас.
|
||||||
|
//
|
||||||
|
// Так даже надёжнее: видно в одном месте, что именно уедет модели, и чужой
|
||||||
|
// потребитель, вклинившийся между нашими шагами, ничего не портит — мы всё
|
||||||
|
// приносим заново. Плата — вопрос растёт с каждым шагом на длину прошлого
|
||||||
|
// ответа; при steps=4 это единицы килобайт против 22 КБ первого сообщения.
|
||||||
|
function growTranscript(prev, answer, next) {
|
||||||
|
return [prev, '',
|
||||||
|
'=== ТВОЙ ПРЕДЫДУЩИЙ ОТВЕТ ===', answer.trim(), '=== END ===', '',
|
||||||
|
next].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAgent(cfg, root, question, files, log) {
|
||||||
let text = await buildFirst(root, files, question);
|
let text = await buildFirst(root, files, question);
|
||||||
let answer = '';
|
let answer = '';
|
||||||
let lastSig = '';
|
let lastSig = '';
|
||||||
|
|
||||||
for (let n = 1; n <= cfg.steps; n++) {
|
for (let n = 1; n <= cfg.steps; n++) {
|
||||||
answer = await callBridge(cfg, text, { fresh: n === 1, temp, n, log });
|
answer = await callDeepseek(cfg, text, { n, log });
|
||||||
const requests = parseRequests(answer);
|
const requests = parseRequests(answer);
|
||||||
if (!requests.length) { log(`шаг ${n}: запросов нет, готово`); break; }
|
if (!requests.length) { log(`шаг ${n}: запросов нет, готово`); break; }
|
||||||
|
|
||||||
@@ -360,11 +377,9 @@ async function runAgent(cfg, root, question, files, log) {
|
|||||||
|
|
||||||
log(`шаг ${n}: запрошено ${requests.length}`);
|
log(`шаг ${n}: запрошено ${requests.length}`);
|
||||||
const result = await runRequests(root, requests, log);
|
const result = await runRequests(root, requests, log);
|
||||||
text = FOLLOW_HEAD + result;
|
text = growTranscript(text, answer, FOLLOW_HEAD + result);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!flags['keep-temp']) await rm(temp, { recursive: true, force: true }).catch(() => {});
|
|
||||||
else log(`временные файлы: ${temp}`);
|
|
||||||
return answer;
|
return answer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,11 +441,14 @@ async function serve(cfg) {
|
|||||||
return json(res, 200, { root, files: await listFiles(root) });
|
return json(res, 200, { root, files: await listFiles(root) });
|
||||||
|
|
||||||
if (req.method === 'GET' && url.pathname === '/api/status') {
|
if (req.method === 'GET' && url.pathname === '/api/status') {
|
||||||
|
// 200 от /api/tags значит «браузер поднят и вкладка на месте»; 503 —
|
||||||
|
// сервис жив, но браузера ещё нет (поднимется на первом же вопросе);
|
||||||
|
// отказ соединения — сервис не запущен вовсе. Три состояния, и плашка
|
||||||
|
// в ui.html должна их различать.
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`http://127.0.0.1:${cfg.cdpPort}/json/list`, { signal: AbortSignal.timeout(1500) });
|
const r = await fetch(`${cfg.deepseekHost}/api/tags`, { signal: AbortSignal.timeout(2500) });
|
||||||
const pages = (await r.json()).filter(t => t.type === 'page');
|
return json(res, 200, { service: true, deepseek: r.ok });
|
||||||
return json(res, 200, { browser: true, deepseek: pages.some(t => (t.url || '').includes('chat.deepseek.com')) });
|
} catch { return json(res, 200, { service: false, deepseek: false }); }
|
||||||
} catch { return json(res, 200, { browser: false, deepseek: false }); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === 'GET' && url.pathname === '/api/progress')
|
if (req.method === 'GET' && url.pathname === '/api/progress')
|
||||||
@@ -533,22 +551,27 @@ async function selftest() {
|
|||||||
const cfg = await readConfig();
|
const cfg = await readConfig();
|
||||||
cfg.steps = Number.parseInt(flags.steps, 10) || cfg.steps;
|
cfg.steps = Number.parseInt(flags.steps, 10) || cfg.steps;
|
||||||
cfg.timeout = Number.parseInt(flags.timeout, 10) || cfg.timeout;
|
cfg.timeout = Number.parseInt(flags.timeout, 10) || cfg.timeout;
|
||||||
if (flags.chat) cfg.chat = String(flags.chat);
|
if (flags.chat) cfg.deepseekModel = 'deepseek:' + String(flags.chat);
|
||||||
|
|
||||||
if (flags.selftest) { await selftest(); process.exit(0); }
|
if (flags.selftest) { await selftest(); process.exit(0); }
|
||||||
|
|
||||||
if (!existsSync(cfg.bridge)) {
|
|
||||||
console.error(`Мост не найден: ${cfg.bridge}`);
|
|
||||||
console.error('Укажите путь к ask.mjs в config.json (образец — config.example.json).');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flags.ui || positional[0] === 'ui') {
|
if (flags.ui || positional[0] === 'ui') {
|
||||||
await serve(cfg);
|
await serve(cfg);
|
||||||
} else if (flags.check) {
|
} else if (flags.check) {
|
||||||
console.log(`мост: ${cfg.bridge}`);
|
// Три состояния, и путать их нельзя: 200 — всё готово; 503 — сервис жив,
|
||||||
const p = spawn(process.execPath, [cfg.bridge, 'probe'], { cwd: dirname(cfg.bridge), stdio: 'inherit' });
|
// но браузер ещё не поднят (встанет на первом вопросе); отказ соединения —
|
||||||
p.on('close', c => process.exit(c));
|
// сервис не запущен вовсе.
|
||||||
|
console.log(`сервис: ${cfg.deepseekHost}`);
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${cfg.deepseekHost}/api/tags`, { signal: AbortSignal.timeout(3000) });
|
||||||
|
console.log(r.ok ? 'готов: браузер поднят, вкладка DeepSeek открыта'
|
||||||
|
: `не готов (HTTP ${r.status}): браузер поднимется на первом вопросе`);
|
||||||
|
process.exit(r.ok ? 0 : 1);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`не отвечает: ${e.message}`);
|
||||||
|
console.error('Запустите start.cmd в папке deepseek-api.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const question = positional.join(' ');
|
const question = positional.join(' ');
|
||||||
if (!question.trim()) { console.log(USAGE); process.exit(question ? 1 : 0); }
|
if (!question.trim()) { console.log(USAGE); process.exit(question ? 1 : 0); }
|
||||||
|
|||||||
@@ -90,8 +90,9 @@ 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());
|
||||||
$('dot').className = 'dot' + (s.deepseek ? '' : ' bad');
|
$('dot').className = 'dot' + (s.deepseek ? '' : ' bad');
|
||||||
$('bar').textContent = s.deepseek ? 'мост на месте, вкладка DeepSeek открыта'
|
$('bar').textContent = s.deepseek ? 'deepseek-api на месте, вкладка DeepSeek открыта'
|
||||||
: s.browser ? 'браузер есть, но вкладки DeepSeek нет' : 'браузер с портом 9333 не найден — запустите start-browser.cmd';
|
: s.service ? 'deepseek-api отвечает, браузер ещё не поднят — встанет на первом вопросе'
|
||||||
|
: 'deepseek-api не запущен — запустите start.cmd в его папке';
|
||||||
} catch { $('dot').className = 'dot bad'; $('bar').textContent = 'пульт не отвечает'; }
|
} catch { $('dot').className = 'dot bad'; $('bar').textContent = 'пульт не отвечает'; }
|
||||||
}
|
}
|
||||||
status(); setInterval(status, 5000);
|
status(); setInterval(status, 5000);
|
||||||
|
|||||||
Reference in New Issue
Block a user