Одна модель вместо трёх: 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:
Alex Cube
2026-09-11 10:26:03 +03:00
co-authored by Claude Opus 5
parent adbcedb485
commit 6ccb5dd46b
6 changed files with 162 additions and 337 deletions
+101 -257
View File
@@ -29,14 +29,14 @@ export function configure({ cdpPort, browserExe, profileDir, chatUrl }) {
if (chatUrl) BROWSER.url = chatUrl;
}
// Роли — это отдельные чаты под разные режимы. Режим выбирается при СОЗДАНИИ
// чата и потом не меняется: тумблеры исчезают после первого сообщения.
export const ROLES = ['think', 'search', 'vision'];
// Модель у DeepSeek теперь одна и универсальная: выбор из трёх (Instant /
// Expert / Vision) убран из веб-морды, картинки разбирает та же модель.
// Поэтому и чат нужен один — рабочий, расходный.
// Роль → id чата из chats.json. Файл — рабочее состояние, а не настройка:
// он заводится сам и переписывается при каждой смене чата.
async function chatsMap() {
try { return JSON.parse(await readFile(CHATS_FILE, 'utf8')); } catch { return {}; }
// Id рабочего чата из chats.json. Файл — состояние, а не настройка: он
// заводится сам и переписывается, когда чат меняется.
async function savedChatId() {
try { return JSON.parse(await readFile(CHATS_FILE, 'utf8')).id || null; } catch { return null; }
}
@@ -181,96 +181,21 @@ function __answerNodes() {
}
return { sel: null, nodes: [] };
}
// Тумблеры DeepThink и Search рядом с полем ввода. Раньше они пропадали, как
// только в чате появлялось первое сообщение, — на этом держалась вся прежняя
// схема с отдельным чатом под каждый режим. Проверено 11.09.2026: теперь они
// видны и переключаются в любом чате, в том числе начатом.
//
// Разметка: div.ds-toggle-button с aria-pressed. Вложенная иконка несёт класс
// ds-toggle-button__icon — это не тумблер, кликать по ней бессмысленно.
function __toggles() {
// В диалоге подписи пропадают и остаются иконки, поэтому опора на класс тумблера,
// а текст/подсказка — лишь для имени.
return [...document.querySelectorAll('div[role="button"], button, [class*="toggle-button"]')]
return [...document.querySelectorAll('.ds-toggle-button')]
.filter(__visible)
.filter(e => {
const c = e.getAttribute('class') || '';
if (/toggle-button__/.test(c)) return false; // вложенные иконки, не сами тумблеры
return /ds-toggle-button/.test(c) || /^(DeepThink|Search)$/i.test((e.innerText || '').trim());
})
.map(e => {
const cls = e.getAttribute('class') || '';
return {
name: (e.innerText || '').trim() ||
e.getAttribute('aria-label') || e.getAttribute('title') ||
(e.querySelector('[aria-label]')?.getAttribute('aria-label')) || '(без подписи)',
// активный тумблер помечается доп. классом либо aria-атрибутом
on: /active|checked|selected|--on\\b/i.test(cls) ||
e.getAttribute('aria-pressed') === 'true' ||
e.getAttribute('aria-checked') === 'true',
cls
};
});
}
function __clickToggle(name) {
// Ищем среди тех же элементов, что показывает __toggles: тумблер — это div
// с классом ds-toggle-button, а не button с role.
const el = [...document.querySelectorAll('div[role="button"], button, [class*="toggle-button"]')]
.filter(__visible)
.filter(e => !/toggle-button__/.test(e.getAttribute('class') || ''))
.find(e => (e.innerText || '').trim().toLowerCase() === name.toLowerCase());
if (!el) return { ok: false, reason: 'кнопка ' + name + ' не найдена' };
el.click();
return { ok: true };
}
// Режим (Search / DeepThink) задаётся при СОЗДАНИИ чата и потом не меняется:
// тумблеры пропадают, как только в чате появилось первое сообщение. Поэтому
// работа идёт через переключение между заранее созданными чатами в сайдбаре.
function __chatItems() {
const byHref = [...document.querySelectorAll('a[href*="/chat/s/"]')].filter(__visible);
if (byHref.length) {
return { how: 'ссылки', items: byHref.map(e => ({
title: (e.innerText || '').replace(/\\s+/g, ' ').trim().slice(0, 80),
href: e.getAttribute('href'),
current: location.href.includes((e.getAttribute('href') || '').split('/').pop())
})) };
}
// Сайдбар без <a>: берём кликабельные строки с текстом из левой колонки.
const rows = [...document.querySelectorAll('div[class*="chat"], div[class*="history"], li')]
.filter(__visible)
.filter(e => e.getBoundingClientRect().left < 320 && e.getBoundingClientRect().width < 340)
.filter(e => { const t = (e.innerText || '').trim(); return t && t.length < 120; });
const uniq = rows.filter(e => !rows.some(o => o !== e && o.contains(e)));
return { how: 'сайдбар', items: uniq.map(e => ({
title: (e.innerText || '').replace(/\\s+/g, ' ').trim().slice(0, 80), href: null, current: false
})) };
}
function __openChat(needle) {
const low = String(needle).toLowerCase();
const cand = [...document.querySelectorAll('a[href*="/chat/s/"]')].filter(__visible)
.find(e => (e.innerText || '').toLowerCase().includes(low) ||
(e.getAttribute('href') || '').toLowerCase().includes(low));
if (cand) { cand.click(); return { ok: true, title: (cand.innerText || '').trim() }; }
const rows = __chatItems().items;
const idx = rows.findIndex(r => r.title.toLowerCase().includes(low));
if (idx === -1) return { ok: false, reason: 'чат не найден: ' + needle };
const all = [...document.querySelectorAll('div[class*="chat"], div[class*="history"], li')]
.filter(__visible).filter(e => (e.innerText || '').replace(/\\s+/g, ' ').trim().slice(0, 80) === rows[idx].title);
if (!all.length) return { ok: false, reason: 'не удалось кликнуть по чату' };
all[0].click();
return { ok: true, title: rows[idx].title };
}
// Индикатор выбранной модели рядом с полем ввода: Instant / DeepThink / Search.
// Это единственный надёжный признак — классы тумблеров врут, а подпись показывает,
// чем чат будет отвечать на самом деле.
function __modelLabel() {
// Выбор модели — это radiogroup: div[data-model-type=default|expert|vision]
// с aria-checked. Это точный источник, подписи и классы вторичны.
const radios = [...document.querySelectorAll('[data-model-type][role="radio"]')];
if (radios.length) {
const on = radios.find(r => r.getAttribute('aria-checked') === 'true');
return on ? [on.getAttribute('data-model-type')] : [];
}
// Чат уже начат: radiogroup спрятан, остаётся только подпись рядом с полем ввода.
const known = /^(instant|expert|vision)$/i;
return [...document.querySelectorAll('span, div')]
.filter(__visible)
.filter(e => !e.children.length && known.test((e.innerText || '').trim()))
.filter(e => !e.closest('[class*="ds-toggle-button"]'))
.map(e => (e.innerText || '').trim().toLowerCase());
.filter(e => !/ds-toggle-button__/.test(e.getAttribute('class') || ''))
.map(e => ({
name: (e.innerText || '').trim() || '(без подписи)',
on: e.getAttribute('aria-pressed') === 'true',
}));
}
// Только координаты: сам клик — настоящими мышиными событиями снаружи.
// Причина найдена замером 26.08.2026: btn.click() из DOM React глотает молча,
@@ -288,17 +213,15 @@ function __newChatBox() {
// data-virtual-list-item-key. Ответ отличается от вопроса тем, что внутри
// него есть .ds-markdown.
//
// За номер ключа цепляться НЕЛЬЗЯ: после каждой правки список
// перенумеровывается (было 1/2, стало 3/4), а невидимых элементов в DOM
// вообще нет — виртуализация их выбрасывает. Берём самый ранний из
// отрисованных вопросов. После правки в чате остаётся ровно один обмен,
// поэтому такой вопрос там один.
// За номер ключа цепляться НЕЛЬЗЯ, и сортировать по нему тоже: номера живут
// своей жизнью — после правок список перенумеровывается, а у вопросов они
// теперь и вовсе отрицательные (замер 11.09.2026: -4, 2, -11, 8 для двух
// обменов подряд). Сортировка по возрастанию отдавала ПОСЛЕДНИЙ вопрос вместо
// первого, и правка уходила не в то сообщение. Берём порядок DOM: список
// рисуется сверху вниз, значит первый отрисованный вопрос — он и есть.
function __firstMsg() {
const items = [...document.querySelectorAll('[data-virtual-list-item-key]')]
.map(e => ({ e, k: Number(e.getAttribute('data-virtual-list-item-key')) }))
.filter(o => Number.isFinite(o.k) && !o.e.querySelector('.ds-markdown'))
.sort((a, b) => a.k - b.k);
return items.length ? items[0].e : null;
return [...document.querySelectorAll('[data-virtual-list-item-key]')]
.find(e => !e.querySelector('.ds-markdown')) || null;
}
// Кнопки под сообщением (копировать, редактировать). Появляются по наведению
// и прячутся, когда курсор уходит вниз к ним, поэтому жать только из DOM.
@@ -628,92 +551,41 @@ async function openChatById(s, id, log) {
throw new Error(`не удалось открыть чат ${id} — страница не загрузилась.`);
}
// Настоящий клик мышью по координатам: el.click() у DeepSeek только красит
// тумблер, но модель чата не меняет — проверяется по индикатору Instant/DeepThink.
async function realClick(s, name) {
// Щёлкнуть по тумблеру настоящим мышиным событием. DOM-вызов click() тут
// тоже срабатывает, но мышь проверена на большем числе случаев и работает
// даже там, где React глотает синтетический клик.
async function clickToggle(s, name) {
const box = await s.eval(`(() => { ${PAGE_HELPERS}
const el = [...document.querySelectorAll('div[role="button"], button, [class*="toggle-button"]')]
const el = [...document.querySelectorAll('.ds-toggle-button')]
.filter(__visible)
.filter(e => !/toggle-button__/.test(e.getAttribute('class') || ''))
.filter(e => !/ds-toggle-button__/.test(e.getAttribute('class') || ''))
.find(e => (e.innerText || '').trim().toLowerCase() === ${JSON.stringify(name)}.toLowerCase());
if (!el) return null;
const r = el.getBoundingClientRect();
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
})()`);
if (!box) return false;
const p = { x: box.x, y: box.y, button: 'left', clickCount: 1 };
await s.send('Input.dispatchMouseEvent', { ...p, type: 'mouseMoved', button: 'none', clickCount: 0 });
await sleep(120);
await s.send('Input.dispatchMouseEvent', { ...p, type: 'mousePressed' });
await s.send('Input.dispatchMouseEvent', { ...p, type: 'mouseReleased' });
await hoverClick(s, box);
await sleep(600);
return true;
}
// Клик мышью по видимому элементу с ТОЧНЫМ текстом (пункт меню моделей).
async function clickExactText(s, text) {
const box = await s.eval(`(() => { ${PAGE_HELPERS}
const el = [...document.querySelectorAll('div, span, li, button')].filter(__visible)
.filter(e => (e.innerText || '').trim() === ${JSON.stringify(text)})
.sort((a, b) => a.getBoundingClientRect().width - b.getBoundingClientRect().width)[0];
if (!el) return null;
const r = el.getBoundingClientRect();
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
})()`);
if (!box) return false;
const p = { x: box.x, y: box.y, button: 'left', clickCount: 1 };
await s.send('Input.dispatchMouseEvent', { ...p, type: 'mouseMoved', button: 'none', clickCount: 0 });
await sleep(120);
await s.send('Input.dispatchMouseEvent', { ...p, type: 'mousePressed' });
await s.send('Input.dispatchMouseEvent', { ...p, type: 'mouseReleased' });
await sleep(700);
return true;
}
// У DeepSeek два независимых механизма: выпадающий выбор МОДЕЛИ
// (Instant / Expert / Vision) и тумблеры DeepThink / Search поверх неё.
async function ensureModel(s, wantModel, wantToggle, log) {
const read = async () => (await s.eval(`(() => { ${PAGE_HELPERS} return __modelLabel(); })()`))[0] || '?';
for (let attempt = 1; attempt <= 3; attempt++) {
const cur = await read();
if (cur.toLowerCase() === wantModel.toLowerCase()) break;
if (attempt === 3) {
throw new Error(`не удалось выбрать модель ${wantModel} — выбрана ${cur}.`);
}
const box = await s.eval(`(() => { ${PAGE_HELPERS}
const el = document.querySelector('[data-model-type="${wantModel}"][role="radio"]');
if (!el || !__visible(el)) return null;
const r = el.getBoundingClientRect();
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
})()`);
if (!box) throw new Error(`переключатель модели ${wantModel} не найден (чат уже начат?)`);
const p = { x: box.x, y: box.y, button: 'left', clickCount: 1 };
await s.send('Input.dispatchMouseEvent', { ...p, type: 'mouseMoved', button: 'none', clickCount: 0 });
await sleep(120);
await s.send('Input.dispatchMouseEvent', { ...p, type: 'mousePressed' });
await s.send('Input.dispatchMouseEvent', { ...p, type: 'mouseReleased' });
await sleep(700);
log(`выбор модели: попытка ${attempt}${await read()}`);
// DeepThink выключаем перед каждым вопросом: он только показывает размышления
// модели и раздувает ответ, а модель и без него та же самая — выбора моделей
// у DeepSeek больше нет.
//
// Search НЕ трогаем намеренно: это тумблер человека. Как поставлен во вкладке,
// так мост и спрашивает — иначе мост отнимал бы единственный оставшийся
// способ управлять поиском в интернете.
async function ensureNoDeepThink(s, log) {
const isOn = () => s.eval(`(() => { ${PAGE_HELPERS}
return __toggles().some(t => /deepthink/i.test(t.name) && t.on); })()`);
if (!await isOn()) return;
if (!await clickToggle(s, 'DeepThink')) {
log('тумблер DeepThink не найден — оставляю как есть');
return;
}
if (wantToggle) {
const before = await s.eval(`(() => { ${PAGE_HELPERS} return __toggles(); })()`);
const t = before.find(x => x.name.toLowerCase() === wantToggle.toLowerCase());
if (t && !t.on) {
await realClick(s, wantToggle);
const after = await s.eval(`(() => { ${PAGE_HELPERS} return __toggles(); })()`);
const now = after.find(x => x.name.toLowerCase() === wantToggle.toLowerCase());
log(`тумблер ${wantToggle}: ${now?.on ? 'включён' : 'НЕ включился'}`);
} else if (t?.on) {
log(`тумблер ${wantToggle}: уже включён`);
}
}
const model = await read();
const toggles = await s.eval(`(() => { ${PAGE_HELPERS} return __toggles(); })()`);
const on = toggles.filter(x => x.on).map(x => x.name);
return on.length ? `${model}+${on.join('+')}` : model;
log(await isOn() ? 'DeepThink выключить не удалось' : 'DeepThink выключен');
}
// Ответ считается готовым, когда его текст перестал меняться.
@@ -925,15 +797,11 @@ async function deleteChat(s, id, log = () => {}) {
// Стоит недорого: список чатов — один eval, и если лишнего нет, дальше ничего
// не происходит.
async function pruneChats(s, log = () => {}) {
let keep = new Set();
try {
const map = JSON.parse(await readFile(CHATS_FILE, 'utf8'));
keep = new Set(Object.values(map).map(v => v && v.id).filter(Boolean));
} catch { return 0; } // нет chats.json — не знаем, что беречь, не трогаем
if (!keep.size) return 0;
const keep = await savedChatId();
if (!keep) return 0; // не знаем, что беречь, — не трогаем ничего
const all = await s.eval(`(() => { ${PAGE_HELPERS} return __chatIds(); })()`);
const extra = all.filter(c => !keep.has(c.id));
const extra = all.filter(c => c.id !== keep);
if (!extra.length) return 0;
log(`лишних чатов в сайдбаре: ${extra.length}, убираю`);
@@ -946,68 +814,48 @@ async function pruneChats(s, log = () => {}) {
return done;
}
// Роль → настройки нового чата. Тот же список, что использует init.
const CHAT_PLAN = {
think: { model: 'expert', toggle: null, hello: 'Рабочий чат для рассуждений. Отвечай кратко.' },
search: { model: 'default', toggle: 'Search', hello: 'Рабочий чат для поиска в интернете. Отвечай кратко.' },
vision: { model: 'vision', toggle: null, hello: 'Рабочий чат для разбора картинок. Отвечай кратко.' },
};
// DeepSeek разрешает править первое сообщение лишь ШЕСТЬ раз, дальше кнопка
// правки перестаёт открывать поле. Поэтому чат — расходник: как только правки
// кончились, заводим новый с тем же режимом и переписываем chats.json.
// кончились, заводим новый и переписываем chats.json.
//
// firstText — настоящий вопрос вместо приветствия. Раньше чат
// всегда открывался служебной фразой, а вопрос уходил ПРАВКОЙ этой фразы: круг
// DeepSeek тратился впустую и одна правка из шести — тоже. Режим чата и его id
// появляются от любого первого сообщения, так что якорь не нужен.
// Возвращает { id, answered } — answered означает, что ответ на firstText уже
// получен и вызывающему остаётся его забрать, а не спрашивать заново.
async function makeChat(s, role, log, firstText = null, timeout = 300) {
const step = CHAT_PLAN[role] || CHAT_PLAN.think;
// Чат сразу открывается настоящим вопросом. Раньше он открывался служебным
// приветствием, а вопрос уходил ПРАВКОЙ этой фразы: круг DeepSeek тратился
// впустую и одна правка из шести — тоже.
async function makeChat(s, log, firstText, timeout = 300, imageBytes = null) {
await clickNewChat(s, log);
await sleep(1500);
await ensureModel(s, step.model, step.toggle, log);
const put = await s.eval(SET_TEXT(firstText || step.hello));
await ensureNoDeepThink(s, log);
const put = await s.eval(SET_TEXT(firstText));
if (!put.ok) throw new Error(put.reason);
if (imageBytes) await attachImage(s, imageBytes, log);
await sleep(300);
await sendAndConfirm(s, log);
const answer = await waitForAnswer(s, put.before,
firstText ? timeout : 420, log, put.textBefore);
const answer = await waitForAnswer(s, put.before, timeout, log, put.textBefore);
// Ждём id, отличный от прежнего. Просто «непустой» тут мало: если переход
// не состоялся, в адресе остаётся старый id, и он выглядит как новый.
const mapFile = CHATS_FILE;
let prev = null;
try { prev = JSON.parse(await readFile(mapFile, 'utf8'))[role]?.id ?? null; } catch { /* нет файла */ }
const old = await savedChatId();
let id = null;
for (let i = 0; i < 20; i++) {
id = await s.eval(`(() => { ${PAGE_HELPERS} return __chatId(); })()`);
if (id && id !== prev) break;
if (id && id !== old) break;
id = null;
await sleep(500);
}
if (!id) throw new Error('новый чат создан, но его id не появился в адресе');
const file = mapFile;
let map = {};
try { map = JSON.parse(await readFile(file, 'utf8')); } catch { /* заведём заново */ }
// Чат — расходник, и раньше отработавшие копились в сайдбаре без счёта: за
// сутки работы Эмилии их набралось семнадцать. Удаляем свой прежний чат этой
// роли сразу, как только новый создан и записан.
const old = map[role] && map[role].id;
map[role] = { id, mode: step.toggle ? `${step.model}+${step.toggle}` : step.model };
await writeFile(file, JSON.stringify(map, null, 2), 'utf8');
log(`создан новый чат ${role}: ${id}`);
await writeFile(CHATS_FILE, JSON.stringify({ id }, null, 2), 'utf8');
log(`создан новый чат: ${id}`);
// Отработавшие чаты копились в сайдбаре без счёта: за сутки работы Эмилии
// их набралось семнадцать. Прежний убираем сразу, как только новый записан.
if (old && old !== id) {
try { await deleteChat(s, old, log); }
catch (e) { log(`старый чат ${old} удалить не вышло: ${e.message}`); }
}
return { id, answer, answered: !!firstText };
return { id, answer };
}
// ==================================================================
@@ -1156,14 +1004,13 @@ async function attachImage(s, bytes, log) {
// Один вопрос — один ответ. Всё остальное (какой чат, жив ли он, не кончились
// ли правки) решается здесь и наружу не торчит.
export async function ask({ prompt, role = 'think', imageBytes = null,
export async function ask({ prompt, imageBytes = null,
timeout = 300, log = () => {} }) {
if (!String(prompt || '').trim()) throw new Error('пустой вопрос');
if (!ROLES.includes(role)) role = 'think';
await ensureBrowser(log);
try {
return await askOnce({ prompt, role, imageBytes, timeout, log });
return await askOnce({ prompt, imageBytes, timeout, log });
} catch (e) {
if (!isLostTab(e)) throw e;
// Вкладку закрыли или браузер уронили посреди работы. Поднимаем заново
@@ -1174,53 +1021,49 @@ export async function ask({ prompt, role = 'think', imageBytes = null,
// успела отправить вопрос, вторая его перепишет, а не задаст дважды.
log(`связь с вкладкой потеряна (${e.message}); поднимаю и повторяю`);
await ensureBrowser(log);
return askOnce({ prompt, role, imageBytes, timeout, log });
return askOnce({ prompt, imageBytes, timeout, log });
}
}
async function askOnce({ prompt, role, imageBytes, timeout, log }) {
async function askOnce({ prompt, imageBytes, timeout, log }) {
return withChat(async (s) => {
await pruneChats(s, log); // сайдбар держим чистым
const known = (await chatsMap())[role]?.id ?? null;
const known = await savedChatId();
const alive = known && await s.eval(`(() => { ${PAGE_HELPERS}
return __chatIds().some(c => c.id === ${JSON.stringify(String(known))}); })()`);
// Чата нужного режима нет — заводим сразу с вопросом. Отдельное приветствие
// стоило бы лишнего круга DeepSeek и одной правки из шести, а нужно не было:
// режим и id чата появляются от любого первого сообщения.
if (!alive) {
log(`чат ${role} не найден, создаю сразу с вопросом`);
const made = await makeChat(s, role, log, prompt, timeout);
// Картинка — всегда НОВЫЙ чат. Вложение цепляется к полю ввода, а правка
// идёт в отдельном поле над ним: в старый чат картинку не перенести, и
// отправка новым сообщением ломала единственный уговор, на котором всё
// держится, — «в чате ровно один обмен». Поймано 11.09.2026: после такого
// сообщения чат оставался из двух обменов, правка уходила в чужой вопрос,
// а наружу отдавался ответ на предыдущий.
//
// Рабочего чата нет — тоже заводим, сразу с вопросом. Отдельное приветствие
// стоило бы лишнего круга DeepSeek и одной правки из шести, а нужно
// не было: id чата появляется от любого первого сообщения.
if (!alive || imageBytes) {
log(imageBytes ? 'вопрос с картинкой — завожу новый чат'
: 'рабочий чат не найден, создаю сразу с вопросом');
const made = await makeChat(s, log, prompt, timeout, imageBytes);
return made.answer;
}
await openChatById(s, String(known), log);
if (imageBytes) await attachImage(s, imageBytes, log);
await ensureNoDeepThink(s, log);
// С картинкой — только новым сообщением: вложение попадает в поле ввода,
// а правка идёт в отдельном поле над ним, и картинка туда не переезжает.
const canEdit = !imageBytes && await s.eval(
`(() => { ${PAGE_HELPERS} return !!__firstMsg(); })()`);
if (canEdit) {
try {
const sent = await editFirstAndSend(s, prompt, log);
return await waitForAnswer(s, sent.before, timeout, log, sent.textBefore);
} catch (e) {
// Единственная ожидаемая причина — исчерпан лимит правок (их шесть).
if (!/поле правки не открылось/.test(String(e.message || e))) throw e;
log('правки в этом чате кончились, завожу новый сразу с вопросом');
const made = await makeChat(s, role, log, prompt, timeout);
return made.answer;
}
try {
const sent = await editFirstAndSend(s, prompt, log);
return await waitForAnswer(s, sent.before, timeout, log, sent.textBefore);
} catch (e) {
// Ожидаемых причин две: исчерпан лимит правок (их шесть) и чат без
// единого сообщения — например, его завели руками.
if (!/поле правки не открылось|первое сообщение чата не найдено/.test(String(e.message || e))) throw e;
log('править нечего или правки кончились, завожу новый чат сразу с вопросом');
const made = await makeChat(s, log, prompt, timeout);
return made.answer;
}
const put = await s.eval(SET_TEXT(prompt));
if (!put.ok) throw new Error(put.reason);
await sleep(250);
await sendAndConfirm(s, log);
return waitForAnswer(s, put.before, timeout, log, put.textBefore);
});
}
@@ -1236,9 +1079,10 @@ export async function probe() {
answerBlocks: a.nodes.length,
generating: __isGenerating(),
chats: __chatIds(),
toggles: __toggles(),
needsLogin: /log ?in|войти|sign in/i.test(document.body.innerText.slice(0, 3000)),
};
})()`);
return { tab: tab.url, ...info, chatsFile: await chatsMap() };
return { tab: tab.url, ...info, workChat: await savedChatId() };
});
}