Одна модель вместо трёх: DeepSeek убрал выбор из веб-морды
DeepSeek больше не даёт выбирать модель: radiogroup Instant / Expert /
Vision (div[data-model-type][role="radio"]) со страницы исчез, оставшаяся
модель универсальна и разбирает в том числе картинки. Вместе с выбором
отпали и три роли — think, search, vision, — а с ними отдельный чат под
каждую: теперь чат один.
Заодно выяснилось, что тумблеры DeepThink и Search больше не пропадают
после первого сообщения — они видны и переключаются в любом чате, в том
числе начатом. На их исчезновении держалась вся прежняя схема с чатами
под роль, так что нужда в ней отпала и по этой причине тоже.
DeepThink выключается перед каждым вопросом: он только показывает
размышления модели и раздувает ответ. Search не трогаем намеренно — это
тумблер человека: как поставлен во вкладке, так мост и спрашивает.
Попутно два бага, которые вылезли на живом прогоне:
- data-virtual-list-item-key у вопросов теперь отрицательные (-4, 2, -11,
8 для двух обменов подряд), и __firstMsg сортировкой по возрастанию
отдавал последний вопрос вместо первого. Правка уходила в чужое
сообщение. Берём порядок DOM — список рисуется сверху вниз.
- Вопрос с картинкой уходил новым сообщением в рабочий чат и ломал
уговор «в чате ровно один обмен», на котором держится правка первого
сообщения. После такого чат оставался из двух обменов, и наружу
отдавался ответ на предыдущий вопрос. Теперь картинка всегда открывает
новый чат, а старый удаляется как обычно.
Снаружи: /api/tags отдаёт одну модель deepseek:chat, имя присланной
клиентом модели не смотрится вовсе, ключ defaultRole из config.json убран,
chats.json стал {"id": "..."} вместо карты ролей. /api/probe показывает
положение тумблеров.
Проверено живым прогоном: обычный вопрос, вопрос с картинкой, снова
обычный — ответы верные, чат ротируется, сайдбар чистится.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ez8B2cR3mbm1fjmAMa8JXS
This commit is contained in:
co-authored by
Claude Opus 5
parent
adbcedb485
commit
6ccb5dd46b
+17
-23
@@ -16,7 +16,7 @@ import { readFile } from 'node:fs/promises';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
import { configure, ask, probe, revive, browserAlive, ROLES } from './browser.mjs';
|
||||
import { configure, ask, probe, revive, browserAlive } from './browser.mjs';
|
||||
|
||||
// fileURLToPath, а не разбор pathname вручную: путь проекта может содержать «#»,
|
||||
// и в URL он приезжает как %23 — самодельный разбор ломается именно на нём.
|
||||
@@ -29,7 +29,6 @@ const cfg = {
|
||||
browserExe: '',
|
||||
profileDir: '',
|
||||
chatUrl: 'https://chat.deepseek.com/',
|
||||
defaultRole: 'think',
|
||||
timeout: 300,
|
||||
// Сторож: поднимать браузер и вкладку сам, не дожидаясь вопроса. Иначе
|
||||
// восстановление происходит внутри запроса, и клиент платит за него
|
||||
@@ -50,7 +49,10 @@ if (process.env.DEEPSEEK_CDP_PORT) cfg.cdpPort = Number(process.env.DEEPSEEK_CDP
|
||||
|
||||
configure(cfg);
|
||||
|
||||
const MODELS = ROLES.map(r => 'deepseek:' + r);
|
||||
// Модель одна: выбор из трёх (Instant / Expert / Vision) DeepSeek из веб-морды
|
||||
// убрал, оставшаяся универсальна — она же разбирает картинки. Имя модели,
|
||||
// которое пришлёт клиент, мы не смотрим вовсе.
|
||||
const MODEL = 'deepseek:chat';
|
||||
|
||||
// Состояние для индикаторов. Обновляется сторожем и запросами; отдаётся
|
||||
// по /api/state, НЕ трогая вкладку — на него можно смотреть хоть раз в секунду.
|
||||
@@ -115,14 +117,6 @@ async function runWatch() {
|
||||
|
||||
// ------------------------------------------------------------------ Ollama
|
||||
|
||||
// Роль из имени модели: "deepseek:search" → "search". Незнакомое имя (клиент
|
||||
// прислал свою старую модель вроде qwen3:8b) — не повод падать: берём обычный
|
||||
// чат, иначе смена движка в чужой программе требовала бы ещё и правки модели.
|
||||
export function roleOf(model) {
|
||||
const tail = String(model || '').split(':').pop().toLowerCase();
|
||||
return ROLES.includes(tail) ? tail : cfg.defaultRole;
|
||||
}
|
||||
|
||||
// Ollama принимает роли system/user/assistant отдельными сообщениями, у чата
|
||||
// DeepSeek такого разделения нет — склеиваем в один текст. Роль assistant тоже
|
||||
// сохраняем: в редких сценариях клиент подкладывает начало ответа.
|
||||
@@ -200,10 +194,10 @@ const server = createServer(async (req, res) => {
|
||||
});
|
||||
}
|
||||
return json(res, 200, {
|
||||
models: MODELS.map(m => ({
|
||||
name: m, model: m, modified_at: now(), size: 0,
|
||||
models: [{
|
||||
name: MODEL, model: MODEL, modified_at: now(), size: 0,
|
||||
details: { family: 'deepseek', parameter_size: 'web', quantization_level: '-' },
|
||||
})),
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -232,16 +226,17 @@ const server = createServer(async (req, res) => {
|
||||
const prompt = flatten(body.messages);
|
||||
if (!prompt) return json(res, 400, { error: 'пустой запрос' });
|
||||
const imageBytes = imageOf(body.messages);
|
||||
const role = imageBytes ? 'vision' : roleOf(body.model);
|
||||
const started = Date.now();
|
||||
console.log(`[${role}] ${prompt.length} симв.${imageBytes ? ' + картинка' : ''}`);
|
||||
console.log(`вопрос ${prompt.length} симв.${imageBytes ? ' + картинка' : ''}`);
|
||||
const answer = await serial(() => track(() => ask({
|
||||
prompt, role, imageBytes, timeout: cfg.timeout,
|
||||
prompt, imageBytes, timeout: cfg.timeout,
|
||||
log: m => console.log(` ${m}`),
|
||||
})));
|
||||
console.log(`[${role}] ответ ${answer.length} симв. за ${
|
||||
console.log(`ответ ${answer.length} симв. за ${
|
||||
Math.round((Date.now() - started) / 1000)} с`);
|
||||
const model = body.model || ('deepseek:' + role);
|
||||
// Имя модели возвращаем то, которое прислал клиент: иначе он решит, что
|
||||
// ответила не та модель, которую он просил.
|
||||
const model = body.model || MODEL;
|
||||
const head = { model, created_at: now() };
|
||||
if (body.stream === false) {
|
||||
return json(res, 200, { ...head, message: { role: 'assistant', content: answer },
|
||||
@@ -258,11 +253,10 @@ const server = createServer(async (req, res) => {
|
||||
const body = await readBody(req);
|
||||
const prompt = [body.system, body.prompt].filter(Boolean).join('\n\n').trim();
|
||||
if (!prompt) return json(res, 400, { error: 'пустой запрос' });
|
||||
const role = roleOf(body.model);
|
||||
const answer = await serial(() => track(() => ask({
|
||||
prompt, role, timeout: cfg.timeout, log: m => console.log(` ${m}`),
|
||||
prompt, timeout: cfg.timeout, log: m => console.log(` ${m}`),
|
||||
})));
|
||||
const head = { model: body.model || ('deepseek:' + role), created_at: now() };
|
||||
const head = { model: body.model || MODEL, created_at: now() };
|
||||
if (body.stream === false) {
|
||||
return json(res, 200, { ...head, response: answer, done: true, done_reason: 'stop' });
|
||||
}
|
||||
@@ -296,7 +290,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
||||
server.listen(cfg.port, cfg.host, () => {
|
||||
console.log(`DeepSeek прикидывается Ollama: http://localhost:${cfg.port}`);
|
||||
if (cfg.host !== '0.0.0.0') console.log(`Слушаем только ${cfg.host}`);
|
||||
console.log(`Модели: ${MODELS.join(', ')}`);
|
||||
console.log(`Модель: ${MODEL}`);
|
||||
console.log(`Браузер: порт ${cfg.cdpPort}, профиль ${cfg.profileDir || '(не задан)'}`);
|
||||
if (cfg.watchdog) {
|
||||
console.log(`Сторож: проверка каждые ${cfg.watchdogSec} с`);
|
||||
|
||||
Reference in New Issue
Block a user