run_agent("name", "задача")',
"agents.cmd.more": "…и ещё {n}",
+ "toggles.title": "🎛 Тумблеры DeepSeek",
+ "toggles.hint": "Нажми, чтобы включить/выключить.",
+ "toggles.search": "🔍 Умный поиск",
+ "toggles.think": "🧠 Глубокие раздумья",
+ "toggles.on": "вкл",
+ "toggles.off": "выкл",
+ "toggles.unknown": "?",
+ "toggles.notFound":
+ "❌ Кнопки не найдены на странице. Открой чат DeepSeek и повтори.",
+ "toggles.noWindow": "📂 Нет активного окна.",
+ "toggles.switched": "Переключено: {name} → {state}",
+ "toggles.failed": "Не удалось переключить: {err}",
// ===== Approval =====
"approval.title": "🔐 Подтверждение команды",
@@ -343,6 +356,8 @@ const STRINGS = {
"question.closed": "Вопрос уже закрыт",
"question.notFound": "Вариант не найден",
"question.accepted": "Принято: {answer}",
+ "question.skip": "🚫 Отказаться отвечать",
+ "question.skipped": "Вопрос отменён",
// ===== Уведомление об ответе AI =====
"ai.truncated": "…(обрезано, всего {n} симв.)",
@@ -388,6 +403,7 @@ const STRINGS = {
"help.cmd.files": "/files <hash> — commit files",
"help.cmd.todos": "/todos — tasks of the active window",
"help.cmd.agents": "/agents — project subagents list",
+ "help.cmd.toggles": "/toggles — DeepSeek search & deep-think toggles",
"help.cmd.screen": "/screen — screenshot of the active window",
"help.cmd.cancel": "/cancel — cancel input / approval",
"help.cmd.help": "/help — this help",
@@ -642,6 +658,18 @@ const STRINGS = {
"agents.cmd.hint":
'💡 Delegate a task: run_agent("name", "task")',
"agents.cmd.more": "…and {n} more",
+ "toggles.title": "🎛 DeepSeek toggles",
+ "toggles.hint": "Tap to enable/disable.",
+ "toggles.search": "🔍 Smart Search",
+ "toggles.think": "🧠 Deep Think",
+ "toggles.on": "on",
+ "toggles.off": "off",
+ "toggles.unknown": "?",
+ "toggles.notFound":
+ "❌ Buttons not found on page. Open a DeepSeek chat and try again.",
+ "toggles.noWindow": "📂 No active window.",
+ "toggles.switched": "Toggled: {name} → {state}",
+ "toggles.failed": "Failed to toggle: {err}",
// ===== Approval =====
"approval.title": "🔐 Command approval",
@@ -687,6 +715,8 @@ const STRINGS = {
"question.closed": "Question already closed",
"question.notFound": "Option not found",
"question.accepted": "Accepted: {answer}",
+ "question.skip": "🚫 Refuse to answer",
+ "question.skipped": "Question cancelled",
// ===== AI response notification =====
"ai.truncated": "…(truncated, {n} chars total)",
diff --git a/botsrc/index.js b/botsrc/index.js
index f3d71a7..db22509 100644
--- a/botsrc/index.js
+++ b/botsrc/index.js
@@ -298,12 +298,18 @@ const _planCallbackTokens = new Map();
const _longProcessTokens = new Map();
let _cbTokenCounter = 0;
let _onQuestionAnswered = null;
+let _onQuestionCancelled = null;
/** Установить колбэк, вызываемый при ответе на вопрос из TG. */
function setOnQuestionAnswered(fn) {
_onQuestionAnswered = typeof fn === "function" ? fn : null;
}
+/** Установить колбэк, вызываемый при отказе отвечать на вопрос из TG. */
+function setOnQuestionCancelled(fn) {
+ _onQuestionCancelled = typeof fn === "function" ? fn : null;
+}
+
/** Экранирование для HTML в тексте вопроса. */
function _qEsc(s) {
return String(s == null ? "" : s)
@@ -357,6 +363,11 @@ async function askQuestion(requestId, questions) {
keyboard.push([{ text: qi + 1 + ") " + label, callback_data: token }]);
});
});
+ // Кнопка "Отказаться отвечать" — отменяет весь вопрос целиком.
+ entry.skipToken = "qs_" + requestId;
+ keyboard.push([
+ { text: _t("question.skip"), callback_data: entry.skipToken },
+ ]);
const res = await telegramBot.sendMessage(text, {
parseMode: "HTML",
@@ -461,6 +472,13 @@ async function _handleCallback(chatId, data, cbq) {
await _handleLongProcessCallback(token, longProcRef, cbq);
return;
}
+
+ // Кнопка "Отказаться отвечать" — отменяет вопрос целиком.
+ if (token.startsWith("qs_")) {
+ await _handleQuestionSkip(token.slice(3), cbq);
+ return;
+ }
+
const ref = _callbackTokens.get(token);
if (!ref) {
await telegramBot.answerCallbackQuery(cbq.id);
@@ -795,6 +813,48 @@ async function _handleLongProcessCallback(token, ref, cbq) {
}
/** Перерисовать сообщение с вопросами: отметить выбранное, убрать лишние кнопки. */
+/** Обработать нажатие "Отказаться отвечать": отменить вопрос целиком. */
+async function _handleQuestionSkip(requestId, cbq) {
+ const entry = _pendingQuestions.get(requestId);
+ await telegramBot.answerCallbackQuery(cbq.id, {
+ text: _t("question.skipped"),
+ });
+ if (!entry) return;
+
+ // Очищаем запись и все связанные токены.
+ _pendingQuestions.delete(requestId);
+ for (const row of entry.tokens || []) {
+ for (const t of row || []) _callbackTokens.delete(t);
+ }
+ if (entry.skipToken) _callbackTokens.delete(entry.skipToken);
+
+ // Обновляем сообщение: заголовок + пометка "отменено", кнопок больше нет.
+ if (entry.messageId) {
+ const lines = [_t("question.title")];
+ entry.questions.forEach((q, qi) => {
+ lines.push("");
+ lines.push(qi + 1 + ". " + _qEsc(q.question));
+ });
+ lines.push("");
+ lines.push("🚫 " + _t("question.skipped"));
+ try {
+ await telegramBot.editMessageText(entry.messageId, lines.join("\n"), {
+ parseMode: "HTML",
+ replyMarkup: { inline_keyboard: [] },
+ });
+ } catch (_) {}
+ }
+
+ // Сообщаем окну, что вопрос отменён (reject промиса).
+ if (typeof _onQuestionCancelled === "function") {
+ try {
+ _onQuestionCancelled(requestId);
+ } catch (err) {
+ _log("error", "onQuestionCancelled error:", err.message);
+ }
+ }
+}
+
async function _refreshQuestionMessage(requestId, entry) {
if (!entry.messageId) return;
const lines = [_t("question.title")];
@@ -819,6 +879,12 @@ async function _refreshQuestionMessage(requestId, entry) {
keyboard.push([{ text: qi + 1 + ") " + label, callback_data: token }]);
});
});
+ // Кнопка "Отказаться отвечать" остаётся, пока вопрос не закрыт.
+ if (entry.skipToken) {
+ keyboard.push([
+ { text: _t("question.skip"), callback_data: entry.skipToken },
+ ]);
+ }
await telegramBot.editMessageText(entry.messageId, lines.join("\n"), {
parseMode: "HTML",
@@ -1342,6 +1408,46 @@ async function _handleSettingsCallback(data, cbq) {
return;
}
+ // ---- Тумблеры DeepSeek ----
+ if (
+ data === "st_tg_search" ||
+ data === "st_tg_think" ||
+ data === "st_tg_refresh"
+ ) {
+ if (data === "st_tg_refresh") {
+ await telegramBot.answerCallbackQuery(cbq.id, {
+ text: _t("common.refresh"),
+ });
+ await _cmdToggles(chatId, msgId);
+ return;
+ }
+ const which = data === "st_tg_search" ? "search" : "think";
+ const name =
+ which === "search" ? _t("toggles.search") : _t("toggles.think");
+ const res = await _toggleDeepSeek(which);
+ if (!res.ok) {
+ await telegramBot.answerCallbackQuery(cbq.id, {
+ text: _t("toggles.failed", { err: res.error || "?" }),
+ showAlert: true,
+ });
+ return;
+ }
+ const st = res.after ? _t("toggles.on") : _t("toggles.off");
+ await telegramBot.answerCallbackQuery(cbq.id, {
+ text: _t("toggles.switched", { name: name, state: st }),
+ });
+ // Обновим клавиатуру с новым состоянием.
+ const state = await _readTogglesState();
+ if (msgId && state) {
+ const text = _t("toggles.title") + "\n\n" + _t("toggles.hint");
+ await telegramBot.editMessageText(msgId, text, {
+ parseMode: "HTML",
+ replyMarkup: _togglesKeyboard(state),
+ });
+ }
+ return;
+ }
+
// ---- Выбор языка бота (/start) ----
if (data.startsWith("st_lang_")) {
const lang = i18n.normalizeLang(data.slice("st_lang_".length));
@@ -1780,6 +1886,139 @@ async function _cmdTodos(chatId, page) {
});
}
+/**
+ * Прочитать состояние тумблеров DeepThink/Search через injectPageJS.
+ * Возвращает { search: bool|null, think: bool|null }.
+ */
+async function _readTogglesState() {
+ const win = windowState.getParentWindow
+ ? windowState.getParentWindow()
+ : windowState.getMainWindow();
+ if (!win || win.isDestroyed()) return null;
+
+ const script = `(function(){
+ const find = (needle) => {
+ const all = document.querySelectorAll('.ds-toggle-button');
+ for (const el of all) {
+ const t = (el.textContent || '').trim().toLowerCase();
+ if (t.indexOf(needle) >= 0) return el;
+ }
+ return null;
+ };
+ const think = find('deepthink');
+ const search = find('search');
+ const state = (el) => el ? el.getAttribute('aria-pressed') === 'true' : null;
+ return { think: state(think), search: state(search) };
+ })()`;
+
+ try {
+ const res = await win.webContents.executeJavaScript(script, true);
+ return res || { think: null, search: null };
+ } catch (err) {
+ _log("error", "_readTogglesState error:", err.message);
+ return null;
+ }
+}
+
+/**
+ * Переключить один тумблер. name: 'search' | 'think'.
+ * Возвращает { ok: bool, before: bool|null, after: bool|null, error?: string }.
+ */
+async function _toggleDeepSeek(name) {
+ const win = windowState.getParentWindow
+ ? windowState.getParentWindow()
+ : windowState.getMainWindow();
+ if (!win || win.isDestroyed()) return { ok: false, error: "noWindow" };
+
+ const needle = name === "think" ? "deepthink" : "search";
+ const script = `(function(){
+ const find = (n) => {
+ const all = document.querySelectorAll('.ds-toggle-button');
+ for (const el of all) {
+ const t = (el.textContent || '').trim().toLowerCase();
+ if (t.indexOf(n) >= 0) return el;
+ }
+ return null;
+ };
+ const el = find('${needle}');
+ if (!el) return { ok: false, error: 'notFound' };
+ const before = el.getAttribute('aria-pressed') === 'true';
+ el.click();
+ return { ok: true, before: before };
+ })()`;
+
+ try {
+ const res = await win.webContents.executeJavaScript(script, true);
+ if (!res || !res.ok)
+ return { ok: false, error: (res && res.error) || "unknown" };
+ // Даём React время отрисовать новое состояние
+ await new Promise((r) => setTimeout(r, 250));
+ const st = await _readTogglesState();
+ const after = st ? (name === "think" ? st.think : st.search) : null;
+ return { ok: true, before: res.before, after: after };
+ } catch (err) {
+ return { ok: false, error: err.message };
+ }
+}
+
+/** Inline-клавиатура с тумблерами. */
+function _togglesKeyboard(state) {
+ const fmt = (on) =>
+ on === true
+ ? "✅ " + _t("toggles.on")
+ : on === false
+ ? "⬜ " + _t("toggles.off")
+ : "❔ " + _t("toggles.unknown");
+ return {
+ inline_keyboard: [
+ [
+ {
+ text: _t("toggles.search") + ": " + fmt(state ? state.search : null),
+ callback_data: "st_tg_search",
+ },
+ ],
+ [
+ {
+ text: _t("toggles.think") + ": " + fmt(state ? state.think : null),
+ callback_data: "st_tg_think",
+ },
+ ],
+ [{ text: "🔄 " + _t("common.refresh"), callback_data: "st_tg_refresh" }],
+ ],
+ };
+}
+
+/** /toggles — показать текущее состояние тумблеров. */
+async function _cmdToggles(chatId, msgId) {
+ const win = windowState.getParentWindow
+ ? windowState.getParentWindow()
+ : windowState.getMainWindow();
+ if (!win || win.isDestroyed()) {
+ return telegramBot.sendMessage(_t("toggles.noWindow"), {
+ parseMode: "HTML",
+ });
+ }
+ const state = await _readTogglesState();
+ if (!state || (state.search == null && state.think == null)) {
+ return telegramBot.sendMessage(_t("toggles.notFound"), {
+ parseMode: "HTML",
+ });
+ }
+ const text = _t("toggles.title") + "\n\n" + _t("toggles.hint");
+ const keyboard = _togglesKeyboard(state);
+ if (msgId) {
+ const r = await telegramBot.editMessageText(msgId, text, {
+ parseMode: "HTML",
+ replyMarkup: keyboard,
+ });
+ if (r && r.success) return r;
+ }
+ return telegramBot.sendMessage(text, {
+ parseMode: "HTML",
+ replyMarkup: keyboard,
+ });
+}
+
/** /agents — список субагентов проекта (из cookie/agents/*.md). */
async function _cmdAgents() {
let agents = [];
@@ -2299,6 +2538,7 @@ async function _cmdHelp() {
_t("help.cmd.files"),
_t("help.cmd.todos"),
_t("help.cmd.agents"),
+ _t("help.cmd.toggles"),
_t("help.cmd.screen"),
_t("help.cmd.cancel"),
_t("help.cmd.help"),
@@ -2796,6 +3036,11 @@ async function _handleIncoming(chatId, text, msg) {
await _cmdAgents();
return;
}
+ // Команда /toggles — управление тумблерами DeepSeek (Search / DeepThink).
+ if (cmd === "/toggles" || cmd === "/toggle") {
+ await _cmdToggles(chatId);
+ return;
+ }
// ---- Ожидание ввода значения настройки ----
if (_settingsWaiting.has(chatId)) {
@@ -3684,6 +3929,7 @@ async function _registerCommands() {
diagnostics: "диагностика",
todos: "список задач",
agents: "субагенты проекта",
+ toggles: "тумблеры DeepSeek",
screen: "скриншот окна",
cancel: "отменить ввод",
},
@@ -3703,6 +3949,7 @@ async function _registerCommands() {
diagnostics: "diagnostics",
todos: "todo list",
agents: "project subagents",
+ toggles: "DeepSeek toggles",
screen: "window screenshot",
cancel: "cancel",
},
@@ -3795,6 +4042,7 @@ module.exports = {
notifyLongProcess,
askQuestion,
setOnQuestionAnswered,
+ setOnQuestionCancelled,
_handleIncoming,
_handleCallback,
_sendToChat,
diff --git a/cookie/agents/changelog-writer.md b/cookie/agents/changelog-writer.md
index 06fd33c..d5ca883 100644
--- a/cookie/agents/changelog-writer.md
+++ b/cookie/agents/changelog-writer.md
@@ -1,6 +1,6 @@
---
name: changelog-writer
-description: Ведёт CHANGELOG.md по правилам проекта. Добавляет записи о новых версиях. Использовать когда нужно задокументировать изменения перед релизом.
+description: Ведёт CHANGELOG.md по правилам проекта. Добавляет записи о новых версиях. Знает расширенный markdown-рендер What's New (таблицы, ==mark==, чек-листы, код, цитаты). Использовать когда нужно задокументировать изменения перед релизом.
tools: read, readLines, edit, write, glob, grep, bash
maxTurns: 25
---
@@ -9,6 +9,11 @@ maxTurns: 25
Твоя задача — превратить набор изменений в правильно оформленную запись,
которую корректно распарсит `src/main/changelog-parser.js` и покажет в What's New.
+**Важно:** модалка What's New теперь использует расширенный markdown-рендер
+(таблицы, `==жёлтая подсветка==`, чек-листы, блоки кода, цитаты, ссылки).
+Полный список возможностей и правила использования — в разделе
+«Возможности рендера What's New» ниже.
+
## Первым делом
**Прочитай `CHANGELOG_GUIDE.md`** — там описан точный формат, который понимает парсер.
@@ -133,6 +138,77 @@ log(diff);
- Детали — вложенными пунктами (отступ 2 пробела).
- Не выдумывай фичи — только то, что реально видно в diff/логе.
+## Возможности рендера What's New (актуально с v5.0.0)
+
+Модалка `whats-new` рендерит markdown сама (не через внешние библиотеки) и
+понимает **расширенный набор синтаксиса**. Используй эти возможности, чтобы
+запись выглядела живой и информативной.
+
+**Поддерживается:**
+
+| Синтаксис | Результат | Когда использовать |
+| ----------------------------------- | ------------------------------ | --------------------------------------- |
+| `==текст==` | 🟡 жёлтая подсветка (``) | Ключевые изменения, «обратите внимание» |
+| `**текст**` | **жирный** | Названия фич, заголовки разделов |
+| `_текст_` | _курсив_ | Акценты, примечания |
+| `~~текст~~` | ~~зачёркнуто~~ | Устаревшие/удалённые вещи |
+| `\`code\`` | инлайн-код | Команды, имена функций, пути |
+| `\`\`\`js ... \`\`\`` | блок кода | Примеры конфигов, сниппеты |
+| `\| a \| b \|` + `\| --- \| --- \|` | таблица GFM | Сравнение, матрицы фич, «до/после» |
+| `> текст` | цитата | Предупреждения, важные заметки |
+| `---` | горизонтальный разделитель | Визуальный отступ между темами |
+| `1. 2. 3.` | нумерованный список | Пошаговые инструкции |
+| `- [x]` / `- [ ]` | ☑ / ☐ чек-лист | Статус фич, чеклист обновления |
+| `[text](https://url)` | ссылка | PR, issue, документация |
+| `https://...` (голый) | автоссылка | Быстрые ссылки |
+| `###` / `##` / `#` | h3 / h4 / h5 | Многоуровневые заголовки |
+
+**Практические примеры для CHANGELOG:**
+
+```markdown
+## [5.0.2] - 2026-10-06
+
+### Added
+
+- **Новая модалка What's New** — теперь понимает ==расширенный markdown==:
+ - таблицы GFM для сравнений;
+ - чек-листы `- [x] / - [ ]`;
+ - блоки кода с подсветкой `\`js`;
+ - жёлтую подсветку `==...==` для ключевых изменений.
+
+### Fixed
+
+- Починили рендер ссылок — теперь `[text](url)` и голые URL работают.
+
+### Changed
+
+- **Поведение кнопки закрытия** — было 1 сек, стало `3 сек` (`CLOSE_LOCK_MS`).
+ > Это сделано, чтобы пользователь успел прочитать нововведения.
+
+### Removed
+
+- ~~Старый markdown-рендер~~ — заменён на расширенный.
+```
+
+**Правила использования новых возможностей:**
+
+1. **Не злоупотребляй** — рендер остаётся минимальным, не превращай
+ CHANGELOG в HTML-вёрстку. `==mark==` — для 1–3 действительно важных
+ пунктов, не для всего подряд.
+2. **Таблицы** — уместны для «до/после», матриц фич, списков сравнения.
+ Простой список из 3 пунктов таблицей делать не надо.
+3. **Чек-листы** — только для реальных чек-листов (план обновления, шаги
+ миграции). Для перечисления фич используй обычные `-` пункты.
+4. **Блоки кода** — с указанием языка (`\`js`, `\`bash`, `\`json`),
+ это чисто для читаемости в исходнике (подсветка не активна).
+5. **Экранирование** — HTML в CHANGELOG экранируется автоматически
+ (`escapeHtml`), не пытайся вставлять `` — используй markdown.
+6. **Парсер не знает про новый рендер** — `src/main/changelog-parser.js`
+ извлекает **только** `###` секции и `- `/`* ` пункты. Таблицы,
+ цитаты, блоки кода — это **свободный текст** внутри пункта или между
+ секциями. Если хочешь, чтобы строка попала в What's New — она должна
+ быть либо `### Section`, либо `- пункт`.
+
## Перепиши CHANGELOG.md — оставь ТОЛЬКО последнюю версию
**КРИТИЧНО:** файл `CHANGELOG.md` должен содержать **ровно одну** запись —
@@ -198,6 +274,15 @@ log(check);
Новая версия должна быть **первой** в выводе.
+Дополнительно проверь, что запись корректно отрендерится в What's New:
+
+- все `==...==`, таблицы и чек-листы — **внутри** `### Section` и/или `- пункт`,
+ иначе парсер их не подхватит;
+- заголовки `## [X.Y.Z] - YYYY-MM-DD` — строго этот формат, иначе версия не попадёт в диапазон;
+- нет `Unreleased`, нет старых версий;
+- если использовал таблицу — разделитель `| --- | --- |` обязателен, иначе рендер
+ выведет строку как простой текст с `|` посередине.
+
## Жёсткие правила
1. **НЕ трогай `package.json`** — версию туда выставит CI из git-тега.
@@ -209,6 +294,11 @@ log(check);
7. **Версия = последний git-тег + 1 в patch** (по умолчанию). Не выдумывай minor/major сам.
8. **НЕ вставляй "Unreleased"** — эта секция не покажется в What's New.
9. **НЕ добавляй историю старых версий** — ни `## [5.0.0]`, ни предыдущих. Только текущая.
+10. **НЕ вставляй сырой HTML** — рендер экранирует теги. Используй только markdown
+ (`==mark==`, `**bold**`, таблицы, чек-листы) — см. раздел «Возможности рендера What's New».
+11. **Расширенный markdown (таблицы, `==mark==`, чек-листы, `> цитаты`) — опционально.**
+ Их можно использовать для выразительности, но не в ущерб читаемости. Простой
+ список фич таблицей не оформляй.
## Формат отчёта родителю
diff --git a/cookie/agents/page-scripter.md b/cookie/agents/page-scripter.md
new file mode 100644
index 0000000..083c71d
--- /dev/null
+++ b/cookie/agents/page-scripter.md
@@ -0,0 +1,52 @@
+---
+name: page-scripter
+description: Инжект JS в страницу браузера (inject_page_js / inject_js) и вызов MCP-инструментов (playwright, VideoAudioServer). Без доступа к файлам, shell и git.
+tools: inject_page_js, inject_js, open_browser_window, mcp_list_servers, mcp_get_tools, mcp_call
+maxTurns: 40
+---
+
+Ты — агент, работающий **исключительно** со страницей в браузере и через MCP. Больше ничего делать не умеешь — и не пытайся.
+
+## Что тебе доступно
+
+**Инжект JS в страницу (основной инструмент):**
+- `injectPageJS(code, windowId?)` — выполнить JS в main world страницы (как F12). Без `windowId` — текущее окно с чатом; с `windowId` — окно, открытое через `openBrowserWindow`. Доступны DOM, `window`, `localStorage`, но **не** `window.electronAPI`. Поддерживает `await`; возвращай JSON-совместимые значения.
+- `injectJS(windowId, code)` — выполнить JS в конкретном Electron-окне (по `windowId` от `openBrowserWindow`).
+- `openBrowserWindow(url, options?)` — открыть страницу в отдельном окне, вернёт `windowId`.
+
+**MCP:**
+- `mcpListServers()` — список серверов и статус.
+- `mcpGetTools(server)` — инструменты сервера с параметрами.
+- `mcpCall(server, tool, args)` — вызвать MCP-инструмент.
+
+MCP-серверы: **playwright** (25 тулов: browser_navigate, browser_evaluate, browser_click, browser_fill_form, browser_snapshot, browser_take_screenshot, browser_network_requests, browser_console_messages и др.), **VideoAudioServer** (27 тулов: extract_audio_from_video, trim_video, convert_video_format, add_subtitles и др.). Сверяйся с `mcpGetTools` перед вызовом.
+
+## Чего НЕЛЬЗЯ
+
+- Читать/писать файлы проекта, shell/bash/pwsh, git, поиск по коду.
+- Любые тулы, кроме перечисленных.
+
+Если задача вне набора — верни, что не можешь выполнить, и какой инструмент нужен.
+
+## Как работаешь
+
+1. Пойми цель.
+2. Проверь окружение: для MCP — `mcpGetTools(server)`; для окна — убедись, что есть `windowId`.
+3. Сделай инжект/вызов: код безопасный, идемпотентный, возвращает JSON; `window.electronAPI` недоступен.
+4. Проверь результат.
+
+## Правила
+
+- Только необходимое, без «на всякий случай».
+- Не выдумывай имена MCP-тулов и параметры.
+- `injectPageJS` упал — 2–3 попытки с исправлением.
+- Возвращай сжатый результат.
+
+## Формат вывода
+
+```
+Готово.
+Что сделал: <кратко>
+Результат: <данные / изменения в DOM>
+Замечания: <если есть>
+```
diff --git a/src/main/ipc.js b/src/main/ipc.js
index f3ba4ac..3e67a75 100644
--- a/src/main/ipc.js
+++ b/src/main/ipc.js
@@ -68,6 +68,28 @@ function requestUserQuestion(sender, questions) {
* Резолв вопроса, отвеченного в Telegram (вызывается из botsrc).
* @returns {boolean} true, если нашли ожидающий вопрос.
*/
+/**
+ * Отмена вопроса из Telegram: пользователь нажал "Отказаться отвечать".
+ * Отклоняет ожидающий promise и просит окно закрыть диалог.
+ * @returns {boolean} true, если нашли ожидающий вопрос.
+ */
+function cancelUserQuestionFromTelegram(requestId) {
+ for (const [key, pending] of pendingUserQuestions) {
+ if (key.endsWith(":" + requestId)) {
+ pendingUserQuestions.delete(key);
+ try {
+ const senderId = Number(key.split(":")[0]);
+ const wc = require("electron").webContents.fromId(senderId);
+ if (wc && !wc.isDestroyed())
+ wc.send("ask-user-question-resolved", { requestId });
+ } catch (_) {}
+ pending.reject(new Error("Пользователь отменил вопрос"));
+ return true;
+ }
+ }
+ return false;
+}
+
function resolveUserQuestionFromTelegram(requestId, answers) {
for (const [key, pending] of pendingUserQuestions) {
if (key.endsWith(":" + requestId)) {
@@ -353,6 +375,12 @@ function registerIpcHandlers() {
resolveUserQuestionFromTelegram(requestId, answers);
});
}
+ // Мост Telegram → окно: отказ отвечать отклоняет тот же promise.
+ if (typeof bot.setOnQuestionCancelled === "function") {
+ bot.setOnQuestionCancelled((requestId) => {
+ cancelUserQuestionFromTelegram(requestId);
+ });
+ }
} catch (_) {}
// Решение пользователя по плану (согласие/отказ из окна).
@@ -658,6 +686,99 @@ function registerIpcHandlers() {
}
});
+ // Переименовать чат (DeepSeek и др. — через DOM сайдбара).
+ // Скрипт: находим ссылку a[href*="/a/chat/s/| " + inline(c) + " | ").join("") + + "
|---|
| " + inline(c) + " | ").join("") + + "
' +
+ escapeHtml(codeBuf.join("\n")) +
+ "",
+ );
+ codeBuf = [];
+ inCode = false;
+ };
+
+ const inline = (t) => {
+ // Код в бэктиках — инлайн: \`code\`
+ const codes = [];
+ let s = escapeHtml(t).replace(/`([^`]+)`/g, (_m, c) => {
+ codes.push(c);
+ return "\u0000CODE" + (codes.length - 1) + "\u0000";
+ });
+ s = s
+ .replace(/\*\*(.+?)\*\*/g, "$1")
+ .replace(/(^|\s)_([^_]+)_(\s|$)/g, "$1$2$3")
+ // \u0416\u0451\u043b\u0442\u0430\u044f \u043f\u043e\u0434\u0441\u0432\u0435\u0442\u043a\u0430: ==\u043c\u0430\u0440\u043a\u0435\u0440==
+ .replace(/==([^=]+)==/g, "$1")
+ .replace(/~~([^~]+)~~/g, "" + codes[i] + "",
+ );
+ return s;
+ };
for (const raw of lines) {
- const line = raw.replace(/\s+$/, '');
+ const line = raw.replace(/\s+$/, "");
+
+ // Блок кода ```
+ if (/^\s*```/.test(line)) {
+ if (inList) {
+ out.push("");
+ inList = false;
+ }
+ if (inOList) {
+ out.push("");
+ inOList = false;
+ }
+ flushTable();
+ if (inCode) flushCode();
+ else inCode = true;
+ continue;
+ }
+ if (inCode) {
+ codeBuf.push(raw);
+ continue;
+ }
+
+ // Таблица GFM
+ if (/^\s*\|.*\|\s*$/.test(line)) {
+ if (inList) {
+ out.push("");
+ inList = false;
+ }
+ if (inOList) {
+ out.push("");
+ inOList = false;
+ }
+ tableBuf.push(line.trim());
+ continue;
+ } else {
+ flushTable();
+ }
+
if (!line) {
- if (inList) { out.push(''); inList = false; }
+ if (inList) {
+ out.push("");
+ inList = false;
+ }
+ if (inOList) {
+ out.push("");
+ inOList = false;
+ }
continue;
}
- const mH = line.match(/^###\s+(.+)$/);
+
+ const mH = line.match(/^(#{1,6})\s+(.+)$/);
if (mH) {
- if (inList) { out.push(''); inList = false; }
- out.push('' + inline(mQ[1]) + "", + ); continue; } + + // Нумерованный список + const mO = line.match(/^\s*\d+[.)]\s+(.+)$/); + if (mO) { + if (inList) { + out.push(""); + inList = false; + } + if (!inOList) { + out.push('