From d76a0358e3d41dcf35d3265205f4c8e2ceeab451 Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Thu, 17 Sep 2026 13:26:05 +0300 Subject: [PATCH 1/4] Add i18n and bot language selection Introduce internationalization for the Telegram bot and add a persistent bot language setting. - Add botsrc/i18n.js: RU/EN string tables, t(), getBotLang(), normalizeLang(). - Localize botsrc/index.js: use i18n.t everywhere, add helpers (_t, _lang), language-choice flow on /start, settings page entries, and callback handling for st_lang_*. - Persist telegramLanguage in src/main/settings-store.js (defaults & read/write), derive system fallback. - Add plan.md describing the i18n design and checks. Behavior: if telegramLanguage not chosen, /start prompts RU/EN; language changes persist to cuckoo-settings.json. --- botsrc/i18n.js | 414 +++++++++++++++++++++++++++++++ botsrc/index.js | 485 ++++++++++++++++++++++++------------- src/main/settings-store.js | 110 +++++---- 3 files changed, 799 insertions(+), 210 deletions(-) create mode 100644 botsrc/i18n.js diff --git a/botsrc/i18n.js b/botsrc/i18n.js new file mode 100644 index 0000000..34ff6d0 --- /dev/null +++ b/botsrc/i18n.js @@ -0,0 +1,414 @@ +/** + * botsrc/i18n.js + * Строки Telegram-бота на русском (ru) и английском (en). + * + * Язык бота хранится в cuckoo-settings.json (ключ telegramLanguage) + * и не зависит от языка UI приложения. + */ +const settingsStore = require("../src/main/settings-store"); + +const STRINGS = { + ru: { + // ===== Общие ===== + "common.saved": "✅ Сохранено: {label}", + "common.saveFailed": "❌ Не удалось сохранить", + "common.error": "Ошибка", + "common.on": "✅ ВКЛ", + "common.off": "⚪ ВЫКЛ", + "common.empty": "(пусто)", + "common.back": "⬅️ Назад", + "common.refresh": "🔄 Обновить", + "common.cancelHint": "/cancel — отмена.", + "common.invalidColor": + "⚠ Некорректный цвет. Ожидается hex вида #8b93ff.", + "common.noWindow": "нет активного окна", + + // ===== /start — выбор языка ===== + "lang.choose": "🌐 Выберите язык бота / Choose bot language", + "lang.ru": "🇷🇺 Русский", + "lang.en": "🇬🇧 English", + "lang.saved": "✅ Язык сохранён: {lang}", + + // ===== /help ===== + "help.title": "🦆 Cookie Code — помощь", + "help.commands": "Команды", + "help.cmd.settings": "/settings — открыть меню настроек", + "help.cmd.status": "/status — статус: окно, проект, процессы", + "help.cmd.stop": "/stop — прервать задачу и убить процессы", + "help.cmd.new": "/new — новый чат", + "help.cmd.diff": "/diff — показать изменения (git diff)", + "help.cmd.diagnostics": "/diagnostics — диагностика интеграции", + "help.cmd.todos": "/todos — список задач активного окна", + "help.cmd.cancel": "/cancel — отменить ввод / подтверждение", + "help.cmd.help": "/help — эта справка", + "help.features": "Возможности", + "help.feat.notify": "• Уведомления о вызовах инструментов", + "help.feat.feed": "• Приём входящих сообщений в чат DeepSeek", + "help.feat.approval": + "• Подтверждение команд — кнопки «Разрешить / Отклонить»", + "help.feat.questions": "• Вопросы от AI с вариантами ответа", + "help.sync": "Настройки синхронизированы с десктопным приложением.", + + // ===== Меню настроек ===== + "settings.root.title": "⚙️ Настройки Cookie Code", + "settings.root.text": "Что настраиваем?", + "settings.page.ui": "🎨 Интерфейс", + "settings.page.glass": "🪟 Стекло и панель", + "settings.page.agent": "🤖 Агент и приватность", + "settings.page.tg": "📡 Telegram-бот", + "settings.page.lang": "🌐 Язык", + + "settings.ui.title": "🎨 Интерфейс", + "settings.ui.text": "Общие визуальные эффекты.", + "settings.glass.title": "🪟 Стекло и панель", + "settings.glass.text": "Прозрачность, размытие и цвета панели Cookie Code.", + "settings.agent.title": "🤖 Агент и приватность", + "settings.agent.text": "Подтверждения, служебные сообщения, форматтеры.", + "settings.tg.title": "📡 Telegram-бот", + "settings.tg.text": "Управление ботом и уведомлениями.", + "settings.lang.title": "🌐 Язык бота", + "settings.lang.text": "Язык меню, команд и уведомлений бота.", + + // Лейблы настроек + "label.customizationEnabled": "Кастомизация вкл", + "label.rgbUsername": "RGB-переливание ника", + "label.language": "Язык UI", + "label.telegramLanguage": "Язык бота", + "label.backgroundBlur": "Размытие фона", + "label.headerBlur": "Размытие шапки", + "label.sidebarBlur": "Размытие сайдбара", + "label.headerOpacity": "Прозрачность шапки", + "label.sidebarOpacity": "Прозрачность сайдбара", + "label.toolBlockOpacity": "Прозрачность tool-блоков", + "label.toolBlockBlur": "Размытие tool-блоков", + "label.overlayOpacity": "Прозрачность панели", + "label.overlayBlur": "Размытие панели", + "label.overlayWidth": "Ширина панели", + "label.overlayBtnRadius": "Скругление кнопок", + "label.overlayBgColor": "Цвет подложки", + "label.overlayPrimaryColor": "Акцентный цвет", + "label.toolApprovalMode": "Подтверждение tools", + "label.hideSystemMessages": "Скрывать служебные сообщения", + "label.formattersEnabled": "Авто-форматтеры", + "label.fileChipEnabled": "Пути как чипы", + "label.showProducedFiles": "Затронутые файлы", + "label.dangerousPatterns": "Опасные паттерны", + "label.telegramEnabled": "Бот включён", + "label.telegramNotifyTools": "Уведомления о tool", + "label.telegramChatFeed": "Принимать из TG", + "label.telegramBotToken": "Токен бота", + "label.telegramChatId": "Chat ID", + + // ===== Ввод значения настройки ===== + "input.prompt": "Отправьте новое значение", + "input.hintOne": + "Отправьте новое значение одним сообщением. /cancel — отмена.", + "input.hintList": + "Отправьте новый список — по одному паттерну в строке. /cancel — отмена.", + "input.current": "Текущее:", + "input.cancelled": "✖️ Ввод отменён.", + "input.nothingToCancel": "Нечего отменять.", + "input.approvalsCancelled": "✖️ Отменено подтверждений: {n}", + + // ===== Результаты колбэков ===== + "cb.enabled": "Включено", + "cb.disabled": "Выключено", + "cb.saved": "Сохранено", + + // ===== /status ===== + "status.title": "📊 Статус Cookie Code", + "status.version": "📦 Версия: {v}", + "status.window": "🪟 Окно: ", + "status.window.ok": "активно", + "status.window.none": "❌ нет", + "status.project": "📁 Проект: ", + "status.project.none": "не выбран", + "status.provider": "🌐 Провайдер: ", + "status.processes": "⚙️ Активных процессов: {n}", + "status.todos": "☑️ Задачи: {done}/{total}", + "status.bot": "📡 Бот: ", + "status.bot.on": "✅ работает", + "status.bot.warn": "⚠️ включён, но не опрашивает", + "status.bot.off": "⚪ выключен", + "status.lastError": "⚠️ Последняя ошибка: ", + + // ===== /stop ===== + "stop.failed": "❌ Не удалось остановить: {err}", + "stop.done": "🛑 Остановлено. Убито процессов: {n}.", + + // ===== /new ===== + "new.done": "🆕 Новый чат открыт.", + + // ===== /diff ===== + "diff.noProject": "⚠️ Проект не выбран.", + "diff.gitUnavailable": "⚠️ {reason}", + "diff.gitDefault": "git недоступен", + "diff.none": "✅ Изменений нет.", + "diff.header": "📝 Изменения ({n})", + "diff.unavailable": "(diff недоступен)", + + // ===== /diagnostics ===== + "diag.title": "🩺 Диагностика", + + // ===== /todos ===== + "todos.empty": "☑ Список задач пуст.", + "todos.header": "☑ Задачи ({done}/{total}):", + + // ===== Уведомления о tool ===== + "tool.editTitle": " ", + "tool.editEmpty": "(пустое изменение)", + "tool.old": "➖ было:", + "tool.new": "➕ стало:", + "tool.truncated": "…(обрезано)", + "tool.truncatedTotal": "…(обрезано, всего {n} симв.)", + "tool.result": "📤 ", + "tool.in": " в ", + "tool.items": " пункт(ов)", + "tool.allDone": "🎉 Все задачи выполнены ({n}/{n})", + + // ===== Approval ===== + "approval.title": "🔐 Подтверждение команды", + "approval.jsScript": "JS-скрипт", + "approval.tool": "инструмент", + "approval.ask": "Разрешить выполнение?", + "approval.allow": "✅ Разрешить", + "approval.deny": "❌ Отклонить", + "approval.allowed": "Команда разрешена", + "approval.denied": "Команда отклонена", + "approval.allowedShort": "✅ Разрешено", + "approval.deniedShort": "❌ Отклонено", + "approval.closed": "Запрос уже закрыт", + + // ===== Вопросы от AI ===== + "question.title": "❓ Вопрос от ИИ", + "question.hint": "Выберите вариант кнопкой ниже.", + "question.closed": "Вопрос уже закрыт", + "question.notFound": "Вариант не найден", + "question.accepted": "Принято: {answer}", + + // ===== Уведомление об ответе AI ===== + "ai.truncated": "…(обрезано, всего {n} симв.)", + + // ===== Тестовое сообщение ===== + "test.send": "👋 Cookie Code: тестовое уведомление. Всё работает.", + }, + + en: { + // ===== Common ===== + "common.saved": "✅ Saved: {label}", + "common.saveFailed": "❌ Failed to save", + "common.error": "Error", + "common.on": "✅ ON", + "common.off": "⚪ OFF", + "common.empty": "(empty)", + "common.back": "⬅️ Back", + "common.refresh": "🔄 Refresh", + "common.cancelHint": "/cancel — cancel.", + "common.invalidColor": + "⚠ Invalid color. Expected hex like #8b93ff.", + "common.noWindow": "no active window", + + // ===== /start — language picker ===== + "lang.choose": "🌐 Выберите язык бота / Choose bot language", + "lang.ru": "🇷🇺 Русский", + "lang.en": "🇬🇧 English", + "lang.saved": "✅ Language saved: {lang}", + + // ===== /help ===== + "help.title": "🦆 Cookie Code — help", + "help.commands": "Commands", + "help.cmd.settings": "/settings — open settings menu", + "help.cmd.status": "/status — status: window, project, processes", + "help.cmd.stop": "/stop — abort task and kill processes", + "help.cmd.new": "/new — new chat", + "help.cmd.diff": "/diff — show changes (git diff)", + "help.cmd.diagnostics": "/diagnostics — integration diagnostics", + "help.cmd.todos": "/todos — tasks of the active window", + "help.cmd.cancel": "/cancel — cancel input / approval", + "help.cmd.help": "/help — this help", + "help.features": "Features", + "help.feat.notify": "• Notifications about tool calls", + "help.feat.feed": "• Receive incoming messages into DeepSeek chat", + "help.feat.approval": "• Command approval — “Allow / Deny” buttons", + "help.feat.questions": "• AI questions with answer options", + "help.sync": "Settings are synced with the desktop app.", + + // ===== Settings menu ===== + "settings.root.title": "⚙️ Cookie Code settings", + "settings.root.text": "What to configure?", + "settings.page.ui": "🎨 Interface", + "settings.page.glass": "🪟 Glass & panel", + "settings.page.agent": "🤖 Agent & privacy", + "settings.page.tg": "📡 Telegram bot", + "settings.page.lang": "🌐 Language", + + "settings.ui.title": "🎨 Interface", + "settings.ui.text": "General visual effects.", + "settings.glass.title": "🪟 Glass & panel", + "settings.glass.text": + "Transparency, blur and colors of the Cookie Code panel.", + "settings.agent.title": "🤖 Agent & privacy", + "settings.agent.text": "Approvals, system messages, formatters.", + "settings.tg.title": "📡 Telegram bot", + "settings.tg.text": "Bot control and notifications.", + "settings.lang.title": "🌐 Bot language", + "settings.lang.text": + "Language of the bot menu, commands and notifications.", + + // Setting labels + "label.customizationEnabled": "Customization", + "label.rgbUsername": "RGB username", + "label.language": "UI language", + "label.telegramLanguage": "Bot language", + "label.backgroundBlur": "Background blur", + "label.headerBlur": "Header blur", + "label.sidebarBlur": "Sidebar blur", + "label.headerOpacity": "Header opacity", + "label.sidebarOpacity": "Sidebar opacity", + "label.toolBlockOpacity": "Tool block opacity", + "label.toolBlockBlur": "Tool block blur", + "label.overlayOpacity": "Panel opacity", + "label.overlayBlur": "Panel blur", + "label.overlayWidth": "Panel width", + "label.overlayBtnRadius": "Button radius", + "label.overlayBgColor": "Background color", + "label.overlayPrimaryColor": "Accent color", + "label.toolApprovalMode": "Tool approval", + "label.hideSystemMessages": "Hide system messages", + "label.formattersEnabled": "Auto-formatters", + "label.fileChipEnabled": "Paths as chips", + "label.showProducedFiles": "Touched files", + "label.dangerousPatterns": "Dangerous patterns", + "label.telegramEnabled": "Bot enabled", + "label.telegramNotifyTools": "Tool notifications", + "label.telegramChatFeed": "Receive from TG", + "label.telegramBotToken": "Bot token", + "label.telegramChatId": "Chat ID", + + // ===== Setting input ===== + "input.prompt": "Send a new value", + "input.hintOne": "Send a new value in one message. /cancel — cancel.", + "input.hintList": + "Send a new list — one pattern per line. /cancel — cancel.", + "input.current": "Current:", + "input.cancelled": "✖️ Input cancelled.", + "input.nothingToCancel": "Nothing to cancel.", + "input.approvalsCancelled": "✖️ Approvals cancelled: {n}", + + // ===== Callback results ===== + "cb.enabled": "Enabled", + "cb.disabled": "Disabled", + "cb.saved": "Saved", + + // ===== /status ===== + "status.title": "📊 Cookie Code status", + "status.version": "📦 Version: {v}", + "status.window": "🪟 Window: ", + "status.window.ok": "active", + "status.window.none": "❌ none", + "status.project": "📁 Project: ", + "status.project.none": "not selected", + "status.provider": "🌐 Provider: ", + "status.processes": "⚙️ Active processes: {n}", + "status.todos": "☑️ Tasks: {done}/{total}", + "status.bot": "📡 Bot: ", + "status.bot.on": "✅ running", + "status.bot.warn": "⚠️ enabled but not polling", + "status.bot.off": "⚪ disabled", + "status.lastError": "⚠️ Last error: ", + + // ===== /stop ===== + "stop.failed": "❌ Failed to stop: {err}", + "stop.done": "🛑 Stopped. Processes killed: {n}.", + + // ===== /new ===== + "new.done": "🆕 New chat opened.", + + // ===== /diff ===== + "diff.noProject": "⚠️ Project not selected.", + "diff.gitUnavailable": "⚠️ {reason}", + "diff.gitDefault": "git unavailable", + "diff.none": "✅ No changes.", + "diff.header": "📝 Changes ({n})", + "diff.unavailable": "(diff unavailable)", + + // ===== /diagnostics ===== + "diag.title": "🩺 Diagnostics", + + // ===== /todos ===== + "todos.empty": "☑ Task list is empty.", + "todos.header": "☑ Tasks ({done}/{total}):", + + // ===== Tool notifications ===== + "tool.editTitle": " ", + "tool.editEmpty": "(empty change)", + "tool.old": "➖ before:", + "tool.new": "➕ after:", + "tool.truncated": "…(truncated)", + "tool.truncatedTotal": "…(truncated, {n} chars total)", + "tool.result": "📤 ", + "tool.in": " in ", + "tool.items": " item(s)", + "tool.allDone": "🎉 All tasks completed ({n}/{n})", + + // ===== Approval ===== + "approval.title": "🔐 Command approval", + "approval.jsScript": "JS script", + "approval.tool": "tool", + "approval.ask": "Allow execution?", + "approval.allow": "✅ Allow", + "approval.deny": "❌ Deny", + "approval.allowed": "Command allowed", + "approval.denied": "Command denied", + "approval.allowedShort": "✅ Allowed", + "approval.deniedShort": "❌ Denied", + "approval.closed": "Request already closed", + + // ===== AI questions ===== + "question.title": "❓ Question from AI", + "question.hint": "Pick an option with the button below.", + "question.closed": "Question already closed", + "question.notFound": "Option not found", + "question.accepted": "Accepted: {answer}", + + // ===== AI response notification ===== + "ai.truncated": "…(truncated, {n} chars total)", + + // ===== Test message ===== + "test.send": "👋 Cookie Code: test notification. All good.", + }, +}; + +/** Нормализовать код языка к 'ru' | 'en'. */ +function normalizeLang(lang) { + return String(lang || "").toLowerCase() === "en" ? "en" : "ru"; +} + +/** Текущий язык бота из настроек (fallback 'ru'). */ +function getBotLang() { + try { + const s = settingsStore.readSettings(); + return normalizeLang(s.telegramLanguage); + } catch (_) { + return "ru"; + } +} + +/** + * Получить строку по ключу для языка lang. + * Поддерживает подстановку {name} из vars. + */ +function t(lang, key, vars) { + const L = normalizeLang(lang); + const table = STRINGS[L] || STRINGS.ru; + let s = table[key]; + if (s == null) s = STRINGS.ru[key] != null ? STRINGS.ru[key] : key; + if (vars && typeof vars === "object") { + s = String(s).replace(/\{(\w+)\}/g, (m, name) => + vars[name] != null ? String(vars[name]) : m, + ); + } + return s; +} + +module.exports = { STRINGS, t, getBotLang, normalizeLang }; diff --git a/botsrc/index.js b/botsrc/index.js index 7cbe5eb..086bb5c 100644 --- a/botsrc/index.js +++ b/botsrc/index.js @@ -11,6 +11,7 @@ const { telegramBot, TelegramBot } = require("./telegram"); const settingsStore = require("../src/main/settings-store"); const windowState = require("../src/main/window"); +const i18n = require("./i18n"); let started = false; @@ -52,9 +53,36 @@ function _read() { notifyTools: !!s.telegramNotifyTools, chatFeed: !!s.telegramChatFeed, approvalMode: s.toolApprovalMode || "off", + lang: i18n.normalizeLang(s.telegramLanguage), }; } +/** Текущий язык бота. */ +function _lang() { + return i18n.getBotLang(); +} + +/** Строка i18n по текущему языку бота. */ +function _t(key, vars) { + return i18n.t(_lang(), key, vars); +} + +/** + * Выбирал ли пользователь язык бота явно. + * Читаем сырой файл настроек: если ключа telegramLanguage нет — язык не выбран. + */ +function _langChosen() { + try { + const fs = require("fs"); + const file = settingsStore.getSettingsPath(); + if (!fs.existsSync(file)) return false; + const parsed = JSON.parse(fs.readFileSync(file, "utf-8")); + return !!(parsed && parsed.telegramLanguage != null); + } catch (_) { + return false; + } +} + function _log(level, ...args) { const tag = "[Cookie Code][telegram]"; const token = (() => { @@ -164,11 +192,15 @@ function _activeSenderId() { /** Текст со списком задач (для /todos и уведомления о завершении). */ function formatTodos(todos) { const items = Array.isArray(todos) ? todos : []; - if (items.length === 0) return "☑ Список задач пуст."; + if (items.length === 0) return _t("todos.empty"); const icon = { pending: "☐", in_progress: "◔", completed: "☑" }; const done = items.filter((t) => t.status === "completed").length; const lines = items.map((t) => (icon[t.status] || "☐") + " " + t.content); - return "☑ Задачи (" + done + "/" + items.length + "):\n" + lines.join("\n"); + return ( + _t("todos.header", { done: done, total: items.length }) + + "\n" + + lines.join("\n") + ); } /** @@ -215,13 +247,13 @@ async function askQuestion(requestId, questions) { const items = Array.isArray(questions) ? questions : []; if (items.length === 0) return { success: false, skipped: true }; - const lines = ["❓ Вопрос от ИИ"]; + const lines = [_t("question.title")]; items.forEach((q, qi) => { lines.push(""); lines.push(qi + 1 + ". " + _qEsc(q.question)); }); lines.push(""); - lines.push("Выберите вариант кнопкой ниже."); + lines.push(_t("question.hint")); const text = lines.join("\n"); const entry = { @@ -288,7 +320,7 @@ async function _handleCallback(chatId, data, cbq) { if (!entry || entry.answers[qi] != null) { _callbackTokens.delete(token); await telegramBot.answerCallbackQuery(cbq.id, { - text: "Вопрос уже закрыт", + text: _t("question.closed"), }); return; } @@ -297,14 +329,16 @@ async function _handleCallback(chatId, data, cbq) { const opt = (q.options || [])[oi]; if (!opt) { await telegramBot.answerCallbackQuery(cbq.id, { - text: "Вариант не найден", + text: _t("question.notFound"), }); return; } entry.answers[qi] = { question: q.question, answer: String(opt.label || "") }; await telegramBot.answerCallbackQuery(cbq.id, { - text: "Принято: " + String(opt.label || "").slice(0, 40), + text: _t("question.accepted", { + answer: String(opt.label || "").slice(0, 40), + }), }); // Обновляем сообщение: показываем выбранные ответы, убираем использованные кнопки. @@ -337,8 +371,8 @@ async function requestApproval(requestId, info) { const isJs = info && info.kind === "js"; const name = isJs - ? "JS-скрипт" - : String((info && info.toolName) || "инструмент"); + ? _t("approval.jsScript") + : String((info && info.toolName) || _t("approval.tool")); let details = ""; try { details = isJs @@ -357,18 +391,20 @@ async function requestApproval(requestId, info) { _approvalCallbackTokens.set(token, { requestId: id, approved: true }); const msg = - "🔐 Подтверждение команды\n" + + _t("approval.title") + + "\n" + escapeHtml(name) + "" + (body ? "\n
" + body + "
" : "") + - "\nРазрешить выполнение?"; + "\n" + + _t("approval.ask"); const res = await telegramBot.sendMessage(msg, { parseMode: "HTML", replyMarkup: { inline_keyboard: [ [ - { text: "✅ Разрешить", callback_data: token }, - { text: "❌ Отклонить", callback_data: token + "d" }, + { text: _t("approval.allow"), callback_data: token }, + { text: _t("approval.deny"), callback_data: token + "d" }, ], ], }, @@ -387,7 +423,7 @@ async function _handleApprovalCallback(ref, cbq) { const entry = _pendingApprovals.get(ref.requestId); if (!entry) { await telegramBot.answerCallbackQuery(cbq.id, { - text: "Запрос уже закрыт", + text: _t("approval.closed"), }); return; } @@ -395,13 +431,15 @@ async function _handleApprovalCallback(ref, cbq) { _approvalCallbackTokens.delete(entry.token); _approvalCallbackTokens.delete(entry.token + "d"); await telegramBot.answerCallbackQuery(cbq.id, { - text: ref.approved ? "Команда разрешена" : "Команда отклонена", + text: ref.approved ? _t("approval.allowed") : _t("approval.denied"), }); if (entry.messageId) { - const status = ref.approved ? "✅ Разрешено" : "❌ Отклонено"; + const status = ref.approved + ? _t("approval.allowedShort") + : _t("approval.deniedShort"); await telegramBot.editMessageText( entry.messageId, - "🔐 Подтверждение команды\n" + status, + _t("approval.title") + "\n" + status, { parseMode: "HTML", replyMarkup: { inline_keyboard: [] }, @@ -429,7 +467,7 @@ function cancelApproval(requestId) { /** Перерисовать сообщение с вопросами: отметить выбранное, убрать лишние кнопки. */ async function _refreshQuestionMessage(requestId, entry) { if (!entry.messageId) return; - const lines = ["❓ Вопрос от ИИ"]; + const lines = [_t("question.title")]; entry.questions.forEach((q, qi) => { lines.push(""); const chosen = entry.answers[qi]; @@ -468,24 +506,29 @@ async function _refreshQuestionMessage(requestId, entry) { const SETTINGS_PAGES = { root: { - title: "⚙️ Настройки Cookie Code", - text: "Что настраиваем?", + titleKey: "settings.root.title", + textKey: "settings.root.text", items: [ - { goto: "ui", label: "🎨 Интерфейс" }, - { goto: "glass", label: "🪟 Стекло и панель" }, - { goto: "agent", label: "🤖 Агент и приватность" }, - { goto: "tg", label: "📡 Telegram-бот" }, + { goto: "ui", labelKey: "settings.page.ui" }, + { goto: "glass", labelKey: "settings.page.glass" }, + { goto: "agent", labelKey: "settings.page.agent" }, + { goto: "tg", labelKey: "settings.page.tg" }, + { goto: "lang", labelKey: "settings.page.lang" }, ], }, ui: { - title: "🎨 Интерфейс", - text: "Общие визуальные эффекты.", + titleKey: "settings.ui.title", + textKey: "settings.ui.text", items: [ - { key: "customizationEnabled", label: "Кастомизация вкл", type: "bool" }, - { key: "rgbUsername", label: "RGB-переливание ника", type: "bool" }, + { + key: "customizationEnabled", + labelKey: "label.customizationEnabled", + type: "bool", + }, + { key: "rgbUsername", labelKey: "label.rgbUsername", type: "bool" }, { key: "language", - label: "Язык UI", + labelKey: "label.language", type: "enum", values: [ ["ru", "🇷🇺 RU"], @@ -496,12 +539,12 @@ const SETTINGS_PAGES = { back: true, }, glass: { - title: "🪟 Стекло и панель", - text: "Прозрачность, размытие и цвета панели Cookie Code.", + titleKey: "settings.glass.title", + textKey: "settings.glass.text", items: [ { key: "backgroundBlur", - label: "Размытие фона", + labelKey: "label.backgroundBlur", type: "number", step: 2, min: 0, @@ -510,7 +553,7 @@ const SETTINGS_PAGES = { }, { key: "headerBlur", - label: "Размытие шапки", + labelKey: "label.headerBlur", type: "number", step: 2, min: 0, @@ -519,7 +562,7 @@ const SETTINGS_PAGES = { }, { key: "sidebarBlur", - label: "Размытие сайдбара", + labelKey: "label.sidebarBlur", type: "number", step: 2, min: 0, @@ -528,7 +571,7 @@ const SETTINGS_PAGES = { }, { key: "headerOpacity", - label: "Прозрачность шапки", + labelKey: "label.headerOpacity", type: "number", step: 5, min: 0, @@ -537,7 +580,7 @@ const SETTINGS_PAGES = { }, { key: "sidebarOpacity", - label: "Прозрачность сайдбара", + labelKey: "label.sidebarOpacity", type: "number", step: 5, min: 0, @@ -546,7 +589,7 @@ const SETTINGS_PAGES = { }, { key: "toolBlockOpacity", - label: "Прозрачность tool-блоков", + labelKey: "label.toolBlockOpacity", type: "number", step: 5, min: 0, @@ -555,7 +598,7 @@ const SETTINGS_PAGES = { }, { key: "toolBlockBlur", - label: "Размытие tool-блоков", + labelKey: "label.toolBlockBlur", type: "number", step: 2, min: 0, @@ -564,7 +607,7 @@ const SETTINGS_PAGES = { }, { key: "overlayOpacity", - label: "Прозрачность панели", + labelKey: "label.overlayOpacity", type: "number", step: 5, min: 0, @@ -573,7 +616,7 @@ const SETTINGS_PAGES = { }, { key: "overlayBlur", - label: "Размытие панели", + labelKey: "label.overlayBlur", type: "number", step: 2, min: 0, @@ -582,7 +625,7 @@ const SETTINGS_PAGES = { }, { key: "overlayWidth", - label: "Ширина панели", + labelKey: "label.overlayWidth", type: "number", step: 20, min: 200, @@ -591,25 +634,33 @@ const SETTINGS_PAGES = { }, { key: "overlayBtnRadius", - label: "Скругление кнопок", + labelKey: "label.overlayBtnRadius", type: "number", step: 2, min: 0, max: 24, unit: "px", }, - { key: "overlayBgColor", label: "Цвет подложки", type: "color" }, - { key: "overlayPrimaryColor", label: "Акцентный цвет", type: "color" }, + { + key: "overlayBgColor", + labelKey: "label.overlayBgColor", + type: "color", + }, + { + key: "overlayPrimaryColor", + labelKey: "label.overlayPrimaryColor", + type: "color", + }, ], back: true, }, agent: { - title: "🤖 Агент и приватность", - text: "Подтверждения, служебные сообщения, форматтеры.", + titleKey: "settings.agent.title", + textKey: "settings.agent.text", items: [ { key: "toolApprovalMode", - label: "Подтверждение tools", + labelKey: "label.toolApprovalMode", type: "enum", values: [ ["off", "⚪ Off"], @@ -619,34 +670,74 @@ const SETTINGS_PAGES = { }, { key: "hideSystemMessages", - label: "Скрывать служебные сообщения", + labelKey: "label.hideSystemMessages", + type: "bool", + }, + { + key: "formattersEnabled", + labelKey: "label.formattersEnabled", + type: "bool", + }, + { + key: "fileChipEnabled", + labelKey: "label.fileChipEnabled", + type: "bool", + }, + { + key: "showProducedFiles", + labelKey: "label.showProducedFiles", type: "bool", }, - { key: "formattersEnabled", label: "Авто-форматтеры", type: "bool" }, - { key: "fileChipEnabled", label: "Пути как чипы", type: "bool" }, - { key: "showProducedFiles", label: "Затронутые файлы", type: "bool" }, { key: "dangerousPatterns", - label: "Опасные паттерны", + labelKey: "label.dangerousPatterns", type: "multiline", }, ], back: true, }, tg: { - title: "📡 Telegram-бот", - text: "Управление ботом и уведомлениями.", + titleKey: "settings.tg.title", + textKey: "settings.tg.text", items: [ - { key: "telegramEnabled", label: "Бот включён", type: "bool" }, - { key: "telegramNotifyTools", label: "Уведомления о tool", type: "bool" }, - { key: "telegramChatFeed", label: "Принимать из TG", type: "bool" }, + { + key: "telegramEnabled", + labelKey: "label.telegramEnabled", + type: "bool", + }, + { + key: "telegramNotifyTools", + labelKey: "label.telegramNotifyTools", + type: "bool", + }, + { + key: "telegramChatFeed", + labelKey: "label.telegramChatFeed", + type: "bool", + }, { key: "telegramBotToken", - label: "Токен бота", + labelKey: "label.telegramBotToken", type: "text", secret: true, }, - { key: "telegramChatId", label: "Chat ID", type: "text" }, + { key: "telegramChatId", labelKey: "label.telegramChatId", type: "text" }, + ], + back: true, + }, + lang: { + titleKey: "settings.lang.title", + textKey: "settings.lang.text", + items: [ + { + key: "telegramLanguage", + labelKey: "label.telegramLanguage", + type: "enum", + values: [ + ["ru", "🇷🇺 Русский"], + ["en", "🇬🇧 English"], + ], + }, ], back: true, }, @@ -686,8 +777,15 @@ function _esc(s) { return escapeHtml(s); } +/** Локализованный лейбл настройки (или её ключ как fallback). */ +function _label(item) { + if (!item) return ""; + if (item.labelKey) return _t(item.labelKey); + return item.label || item.key || ""; +} + function _formatValue(item, value) { - if (item.type === "bool") return value ? "✅ ВКЛ" : "⚪ ВЫКЛ"; + if (item.type === "bool") return value ? _t("common.on") : _t("common.off"); if (item.type === "enum") { const found = (item.values || []).find(([v]) => v === value); return found ? found[1] : String(value); @@ -696,9 +794,9 @@ function _formatValue(item, value) { if (item.type === "color") return String(value || ""); if (item.type === "text" || item.type === "multiline") { if (item.secret) - return value ? "••••" + String(value).slice(-4) : "(пусто)"; + return value ? "••••" + String(value).slice(-4) : _t("common.empty"); const s = String(value == null ? "" : value); - return s.length > 20 ? s.slice(0, 20) + "…" : s || "(пусто)"; + return s.length > 20 ? s.slice(0, 20) + "…" : s || _t("common.empty"); } return String(value); } @@ -706,23 +804,36 @@ function _formatValue(item, value) { function _renderPage(pageName) { const page = SETTINGS_PAGES[pageName]; if (!page) return _renderPage("root"); - const lines = [page.title, "", page.text]; + const lines = [ + page.titleKey ? _t(page.titleKey) : page.title, + "", + page.textKey ? _t(page.textKey) : page.text, + ]; const keyboard = []; if (page.items) { for (const item of page.items) { + // Пункт-переход на другую страницу. + if (item.goto) { + keyboard.push([ + { + text: _label(item), + callback_data: "st_page_" + item.goto, + }, + ]); + continue; + } const v = _getVal(item.key); + const lbl = _label(item); if (item.type === "bool") { const icon = v ? "✅" : "⚪"; keyboard.push([ - { text: icon + " " + item.label, callback_data: "st_t_" + item.key }, + { text: icon + " " + lbl, callback_data: "st_t_" + item.key }, ]); } else if (item.type === "enum") { const cur = _formatValue(item, v); - keyboard.push([ - { text: item.label + ": " + cur, callback_data: "st_noop" }, - ]); - const opts = (item.values || []).map(([val, lbl]) => ({ - text: lbl, + keyboard.push([{ text: lbl + ": " + cur, callback_data: "st_noop" }]); + const opts = (item.values || []).map(([val, lblVal]) => ({ + text: lblVal, callback_data: "st_s_" + item.key + "__" + val, })); if (opts.length) keyboard.push(opts); @@ -730,18 +841,18 @@ function _renderPage(pageName) { const cur = _formatValue(item, v); keyboard.push([ { text: "➖", callback_data: "st_n_" + item.key + "_-" }, - { text: item.label + ": " + cur, callback_data: "st_noop" }, + { text: lbl + ": " + cur, callback_data: "st_noop" }, { text: "➕", callback_data: "st_n_" + item.key + "_+" }, ]); } else if (item.type === "color") { keyboard.push([ - { text: "✏️ " + item.label, callback_data: "st_e_" + item.key }, + { text: "✏️ " + lbl, callback_data: "st_e_" + item.key }, { text: String(v || ""), callback_data: "st_noop" }, ]); } else if (item.type === "text" || item.type === "multiline") { keyboard.push([ { - text: "✏️ " + item.label + ": " + _formatValue(item, v), + text: "✏️ " + lbl + ": " + _formatValue(item, v), callback_data: "st_e_" + item.key, }, ]); @@ -749,17 +860,12 @@ function _renderPage(pageName) { } } if (page.back) { - keyboard.push([{ text: "⬅️ Назад", callback_data: "st_page_root" }]); + keyboard.push([{ text: _t("common.back"), callback_data: "st_page_root" }]); } else { + // Корневая страница — переходы уже добавлены из items (goto). keyboard.push([ - { text: "🔧 Агент", callback_data: "st_page_agent" }, - { text: "🎨 UI", callback_data: "st_page_ui" }, + { text: _t("common.refresh"), callback_data: "st_page_root" }, ]); - keyboard.push([ - { text: "🪟 Стекло", callback_data: "st_page_glass" }, - { text: "📡 Telegram", callback_data: "st_page_tg" }, - ]); - keyboard.push([{ text: "🔄 Обновить", callback_data: "st_page_root" }]); } return { text: lines.join("\n"), keyboard: { inline_keyboard: keyboard } }; } @@ -792,6 +898,30 @@ async function _handleSettingsCallback(data, cbq) { const msgId = cbq.message && cbq.message.message_id; const state = _settingsState.get(chatId) || { page: "root" }; + // ---- Выбор языка бота (/start) ---- + if (data.startsWith("st_lang_")) { + const lang = i18n.normalizeLang(data.slice("st_lang_".length)); + const ok = _setVal("telegramLanguage", lang); + await telegramBot.answerCallbackQuery(cbq.id, { + text: ok + ? i18n.t(lang, "lang.saved", { lang: i18n.t(lang, "lang." + lang) }) + : i18n.t(lang, "common.error"), + }); + // Убираем клавиатуру выбора и показываем помощь на выбранном языке. + if (msgId) { + try { + await telegramBot.editMessageText( + msgId, + i18n.t(lang, "lang.saved", { + lang: i18n.t(lang, "lang." + lang), + }), + { parseMode: "HTML", replyMarkup: { inline_keyboard: [] } }, + ); + } catch (_) {} + } + await _cmdHelp(); + return; + } if (data.startsWith("st_page_")) { await telegramBot.answerCallbackQuery(cbq.id); await _showSettingsMenu(chatId, data.slice("st_page_".length), msgId); @@ -802,7 +932,11 @@ async function _handleSettingsCallback(data, cbq) { const next = !_getVal(key); const ok = _setVal(key, next); await telegramBot.answerCallbackQuery(cbq.id, { - text: ok ? (next ? "Включено" : "Выключено") : "Ошибка", + text: ok + ? next + ? _t("cb.enabled") + : _t("cb.disabled") + : _t("common.error"), }); if (ok) { try { @@ -819,7 +953,7 @@ async function _handleSettingsCallback(data, cbq) { const val = rest.slice(sep + 2); const ok = _setVal(key, val); await telegramBot.answerCallbackQuery(cbq.id, { - text: ok ? "Сохранено" : "Ошибка", + text: ok ? _t("cb.saved") : _t("common.error"), }); if (ok && key === "language") { try { @@ -848,7 +982,7 @@ async function _handleSettingsCallback(data, cbq) { if (typeof item.max === "number") val = Math.min(item.max, val); const ok = _setVal(key, val); await telegramBot.answerCallbackQuery(cbq.id, { - text: ok ? String(val) + (item.unit || "") : "Ошибка", + text: ok ? String(val) + (item.unit || "") : _t("common.error"), }); await _showSettingsMenu(chatId, state.page, msgId); return; @@ -862,13 +996,13 @@ async function _handleSettingsCallback(data, cbq) { } _settingsWaiting.set(chatId, { key }); await telegramBot.answerCallbackQuery(cbq.id, { - text: "Отправьте новое значение", + text: _t("input.prompt"), }); const cur = _getVal(key); const hint = found.item.type === "multiline" - ? "Отправьте новый список — по одному паттерну в строке. /cancel — отмена." - : "Отправьте новое значение одним сообщением. /cancel — отмена."; + ? _t("input.hintList") + : _t("input.hintOne"); const curText = found.item.type === "multiline" ? Array.isArray(cur) @@ -877,10 +1011,12 @@ async function _handleSettingsCallback(data, cbq) { : String(cur == null ? "" : cur); await telegramBot.sendMessage( "✏️ " + - _esc(found.item.label) + + _esc(_label(found.item)) + "\n" + hint + - "\n\nТекущее:\n
" +
+        "\n\n" +
+        _t("input.current") +
+        "\n
" +
         _esc(curText.slice(0, 1500)) +
         "
", { parseMode: "HTML" }, @@ -915,41 +1051,43 @@ async function _cmdStop() { } catch (err) { _log("error", "/stop error:", err.message); return telegramBot.sendMessage( - "❌ Не удалось остановить: " + escapeHtml(err.message), + _t("stop.failed", { err: escapeHtml(err.message) }), ); } - return telegramBot.sendMessage( - "🛑 Остановлено. Убито процессов: " + count + ".", - { parseMode: "HTML" }, - ); + return telegramBot.sendMessage(_t("stop.done", { n: count }), { + parseMode: "HTML", + }); } /** /status — окно, проект, процессы, версия, polling. */ async function _cmdStatus() { const cfg = _read(); - const lines = ["📊 Статус Cookie Code", ""]; + const lines = [_t("status.title"), ""]; let version = "?"; try { version = require("electron").app.getVersion(); } catch (_) {} - lines.push("📦 Версия: " + escapeHtml(version) + ""); + lines.push(_t("status.version", { v: escapeHtml(version) })); const win = windowState.getMainWindow(); const winOk = !!(win && !win.isDestroyed()); - lines.push("🪟 Окно: " + (winOk ? "активно" : "❌ нет")); + lines.push( + _t("status.window") + + (winOk ? _t("status.window.ok") : _t("status.window.none")), + ); const ctx = _activeContext(); const projectDir = ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; lines.push( - "📁 Проект: " + + _t("status.project") + (projectDir ? "" + escapeHtml(projectDir) + "" - : "не выбран"), + : _t("status.project.none")), ); if (ctx && ctx.providerId) - lines.push("🌐 Провайдер: " + escapeHtml(ctx.providerId)); + lines.push(_t("status.provider") + escapeHtml(ctx.providerId)); let procCount = 0; try { @@ -958,7 +1096,7 @@ async function _cmdStatus() { (processManager.activeProcesses && processManager.activeProcesses.size) || 0; } catch (_) {} - lines.push("⚙️ Активных процессов: " + procCount + ""); + lines.push(_t("status.processes", { n: procCount })); let todos = 0, done = 0; @@ -970,20 +1108,20 @@ async function _cmdStatus() { done = list.filter((t) => t.status === "completed").length; } } catch (_) {} - lines.push("☑️ Задачи: " + done + "/" + todos); + lines.push(_t("status.todos", { done: done, total: todos })); const st = telegramBot.getStatus(); lines.push( - "📡 Бот: " + + _t("status.bot") + (cfg.enabled ? st.polling - ? "✅ работает" - : "⚠️ включён, но не опрашивает" - : "⚪ выключен"), + ? _t("status.bot.on") + : _t("status.bot.warn") + : _t("status.bot.off")), ); if (st.lastError) lines.push( - "⚠️ Последняя ошибка: " + escapeHtml(String(st.lastError).slice(0, 200)), + _t("status.lastError") + escapeHtml(String(st.lastError).slice(0, 200)), ); return telegramBot.sendMessage(lines.join("\n"), { parseMode: "HTML" }); @@ -999,9 +1137,9 @@ async function _cmdNew() { const inner = res.result || {}; if (inner.success === false) return telegramBot.sendMessage( - "⚠️ " + escapeHtml(inner.error || "не удалось"), + "⚠️ " + escapeHtml(inner.error || _t("common.error")), ); - return telegramBot.sendMessage("🆕 Новый чат открыт."); + return telegramBot.sendMessage(_t("new.done")); } /** /diff — показать текущие изменения (git diff) в проекте. */ @@ -1009,16 +1147,18 @@ async function _cmdDiff() { const ctx = _activeContext(); const projectDir = ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; - if (!projectDir) return telegramBot.sendMessage("⚠️ Проект не выбран."); + if (!projectDir) return telegramBot.sendMessage(_t("diff.noProject")); const gitDiff = require("../src/main/git-diff"); const status = await gitDiff.getStatus(projectDir); if (!status.success) return telegramBot.sendMessage( - "⚠️ " + escapeHtml(status.reason || "git недоступен"), + _t("diff.gitUnavailable", { + reason: escapeHtml(status.reason || _t("diff.gitDefault")), + }), ); const files = status.files || []; - if (files.length === 0) return telegramBot.sendMessage("✅ Изменений нет."); + if (files.length === 0) return telegramBot.sendMessage(_t("diff.none")); const chunks = []; let total = 0; @@ -1032,9 +1172,9 @@ async function _cmdDiff() { total += text.length; } - const header = "📝 Изменения (" + files.length + ")\n"; + const header = _t("diff.header", { n: files.length }) + "\n"; const fileList = files.map((f) => "• " + escapeHtml(f.path)).join("\n"); - const body = chunks.join("\n\n") || "(diff недоступен)"; + const body = chunks.join("\n\n") || _t("diff.unavailable"); const msg = header + "
" + @@ -1052,39 +1192,55 @@ async function _cmdDiagnostics() { ); if (!res.success) return telegramBot.sendMessage("⚠️ " + escapeHtml(res.error)); - const text = String(res.result || "(пусто)").slice(0, 3500); + const text = String(res.result || _t("common.empty")).slice(0, 3500); return telegramBot.sendMessage( - "🩺 Диагностика\n
" + escapeHtml(text) + "
", + _t("diag.title") + "\n
" + escapeHtml(text) + "
", { parseMode: "HTML" }, ); } async function _cmdHelp() { const text = [ - "🦆 Cookie Code — помощь", + _t("help.title"), "", - "Команды", - "/settings — открыть меню настроек", - "/status — статус: окно, проект, процессы", - "/stop — прервать задачу и убить процессы", - "/new — новый чат", - "/diff — показать изменения (git diff)", - "/diagnostics — диагностика интеграции", - "/todos — список задач активного окна", - "/cancel — отменить ввод / подтверждение", - "/help — эта справка", + _t("help.commands"), + _t("help.cmd.settings"), + _t("help.cmd.status"), + _t("help.cmd.stop"), + _t("help.cmd.new"), + _t("help.cmd.diff"), + _t("help.cmd.diagnostics"), + _t("help.cmd.todos"), + _t("help.cmd.cancel"), + _t("help.cmd.help"), "", - "Возможности", - "• Уведомления о вызовах инструментов", - "• Приём входящих сообщений в чат DeepSeek", - "• Подтверждение команд — кнопки «Разрешить / Отклонить»", - "• Вопросы от AI с вариантами ответа", + _t("help.features"), + _t("help.feat.notify"), + _t("help.feat.feed"), + _t("help.feat.approval"), + _t("help.feat.questions"), "", - "Настройки синхронизированы с десктопным приложением.", + _t("help.sync"), ].join("\n"); return telegramBot.sendMessage(text, { parseMode: "HTML" }); } +/** Показать выбор языка бота (/start при первом запуске). */ +async function _cmdChooseLang() { + const text = _t("lang.choose"); + return telegramBot.sendMessage(text, { + parseMode: "HTML", + replyMarkup: { + inline_keyboard: [ + [ + { text: i18n.t("ru", "lang.ru"), callback_data: "st_lang_ru" }, + { text: i18n.t("ru", "lang.en"), callback_data: "st_lang_en" }, + ], + ], + }, + }); +} + async function _cmdSettings(chatId) { _settingsState.set(chatId, { page: "root" }); return _showSettingsMenu(chatId, "root"); @@ -1110,10 +1266,9 @@ async function _handleSettingsInput(chatId, text) { } else if (item.type === "color") { value = text.trim(); if (!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value)) { - await telegramBot.sendMessage( - "⚠ Некорректный цвет. Ожидается hex вида #8b93ff.", - { parseMode: "HTML" }, - ); + await telegramBot.sendMessage(_t("common.invalidColor"), { + parseMode: "HTML", + }); return true; } } else if (item.type === "text") { @@ -1132,8 +1287,8 @@ async function _handleSettingsInput(chatId, text) { } await telegramBot.sendMessage( ok - ? "✅ Сохранено: " + _esc(item.label) + "" - : "❌ Не удалось сохранить", + ? _t("common.saved", { label: _esc(_label(item)) }) + : _t("common.saveFailed"), { parseMode: "HTML" }, ); const state = _settingsState.get(chatId); @@ -1149,7 +1304,16 @@ async function _handleIncoming(chatId, text) { const cmd = raw.trim().toLowerCase(); // ---- Служебные команды ---- - if (cmd === "/help" || cmd === "/start") { + if (cmd === "/start") { + // При первом запуске предлагаем выбрать язык бота. + if (!_langChosen()) { + await _cmdChooseLang(); + return; + } + await _cmdHelp(); + return; + } + if (cmd === "/help") { await _cmdHelp(); return; } @@ -1185,12 +1349,12 @@ async function _handleIncoming(chatId, text) { const r = cancelApproval(id); if (r && r.success && !r.skipped) cancelledApprovals++; } - if (hadSettings) await telegramBot.sendMessage("✖️ Ввод отменён."); + if (hadSettings) await telegramBot.sendMessage(_t("input.cancelled")); else if (cancelledApprovals > 0) await telegramBot.sendMessage( - "✖️ Отменено подтверждений: " + cancelledApprovals, + _t("input.approvalsCancelled", { n: cancelledApprovals }), ); - else await telegramBot.sendMessage("Нечего отменять."); + else await telegramBot.sendMessage(_t("input.nothingToCancel")); return; } // Команда /todos — показать список задач активного окна. @@ -1223,9 +1387,7 @@ async function _handleIncoming(chatId, text) { _log("info", "incoming → chat:", text); const res = await _sendToChat(text); if (!res.success) { - await telegramBot.sendMessage( - "⚠ Не удалось отправить в чат: " + (res.error || "unknown"), - ); + await telegramBot.sendMessage("⚠ " + (res.error || "unknown")); } } @@ -1238,11 +1400,8 @@ async function notifyAllDone(todos) { if (items.length === 0) return { success: false, skipped: true }; const lines = items.map((t) => "☑ " + escapeHtml(t.content)).join("\n"); const msg = - "🎉 Все задачи выполнены (" + - items.length + - "/" + - items.length + - ")\n
" + + _t("tool.allDone", { n: items.length }) + + "\n
" + lines + "
"; return telegramBot.sendMessage(msg, { parseMode: "HTML" }); @@ -1265,16 +1424,18 @@ function formatToolArgs(toolName, args) { if (file) s += "📄 " + file + "\n"; if (old) s += - "➖ было:\n" + + _t("tool.old") + + "\n" + old.slice(0, 500) + - (old.length > 500 ? "\n…(обрезано)" : "") + + (old.length > 500 ? "\n" + _t("tool.truncated") : "") + "\n"; if (neu) s += - "➕ стало:\n" + + _t("tool.new") + + "\n" + neu.slice(0, 2000) + (neu.length > 2000 - ? "\n…(обрезано, всего " + neu.length + " симв.)" + ? "\n" + _t("tool.truncatedTotal", { n: neu.length }) : ""); return s; } @@ -1286,17 +1447,17 @@ function formatToolArgs(toolName, args) { return ( "🔎 " + (a.pattern || "") + - (a.path ? " в " + a.path : "") + + (a.path ? _t("tool.in") + a.path : "") + (a.include ? " (" + a.include + ")" : "") ); case "glob": - return "🔎 " + (a.pattern || "") + (a.path ? " в " + a.path : ""); + return "🔎 " + (a.pattern || "") + (a.path ? _t("tool.in") + a.path : ""); case "bash": case "pwsh": return "💻 " + (a.command || ""); case "todoWrite": return ( - "☑ " + (Array.isArray(a.todos) ? a.todos.length + " пункт(ов)" : "") + "☑ " + (Array.isArray(a.todos) ? a.todos.length + _t("tool.items") : "") ); default: { // Общий случай — компактный JSON, кроме длинных полей. @@ -1352,7 +1513,7 @@ async function notifyToolResult(toolName, ok, detail) { }; if (oldS) addLines("➖ ", oldS.slice(0, 1000)); if (newS) addLines("➕ ", newS.slice(0, 2000)); - const body = lines.join("\n") || "(пустое изменение)"; + const body = lines.join("\n") || _t("tool.editEmpty"); const title = file ? " " + escapeHtml(file) : ""; const msg = emoji + @@ -1369,7 +1530,7 @@ async function notifyToolResult(toolName, ok, detail) { if (argsText) bodyParts.push(argsText); if (preview) bodyParts.push( - "📤 " + + _t("tool.result") + String(preview) .replace(/\n{3,}/g, "\n\n") .slice(0, 600), @@ -1453,7 +1614,7 @@ async function notifyAIResponse(text) { const MAX = 3500; const body = s.length > MAX - ? s.slice(0, MAX) + "\n…(обрезано, всего " + s.length + " симв.)" + ? s.slice(0, MAX) + "\n" + _t("ai.truncated", { n: s.length }) : s; return telegramBot.sendMessage("🤖 " + body); } @@ -1469,9 +1630,7 @@ async function ping() { async function testSend() { const cfg = _read(); telegramBot.configure(cfg.token, cfg.chatId); - return telegramBot.sendMessage( - "👋 Cookie Code: тестовое уведомление. Всё работает.", - ); + return telegramBot.sendMessage(_t("test.send")); } module.exports = { diff --git a/src/main/settings-store.js b/src/main/settings-store.js index 2cb7cd3..7a80ca8 100644 --- a/src/main/settings-store.js +++ b/src/main/settings-store.js @@ -6,55 +6,56 @@ * Пример содержимого: * { "background": "miku" } */ -const fs = require('fs'); -const path = require('path'); -const { app } = require('electron'); +const fs = require("fs"); +const path = require("path"); +const { app } = require("electron"); const DEFAULT_DANGEROUS_PATTERNS = [ - '^rm\\s+-rf\\s+/', - '^format\\s+', - '^del\\s+/f', - '^rd\\s+/s', - '^shutdown\\s+', - '^taskkill\\s+', - '^diskpart', - '^reg\\s+delete', - '^cipher\\s+/w', + "^rm\\s+-rf\\s+/", + "^format\\s+", + "^del\\s+/f", + "^rd\\s+/s", + "^shutdown\\s+", + "^taskkill\\s+", + "^diskpart", + "^reg\\s+delete", + "^cipher\\s+/w", ]; const DEFAULTS = { customizationEnabled: true, - background: 'miku', - backgroundBlur: 0, // px — размытие самой картинки фона - headerBlur: 12, // px — стекло верхней панели - sidebarBlur: 12, // px — стекло левого сайдбара - headerOpacity: 45, // % — плотность фона шапки (0 = прозрачно) - sidebarOpacity: 45, // % — плотность фона сайдбара (0 = прозрачно) - toolBlockOpacity: 55, // % — плотность фона tool-блоков в чате - toolBlockBlur: 0, // px — стекло tool-блоков в чате - rgbUsername: true, // RGB-переливание ника пользователя (по умолчанию вкл) + background: "miku", + backgroundBlur: 0, // px — размытие самой картинки фона + headerBlur: 12, // px — стекло верхней панели + sidebarBlur: 12, // px — стекло левого сайдбара + headerOpacity: 45, // % — плотность фона шапки (0 = прозрачно) + sidebarOpacity: 45, // % — плотность фона сайдбара (0 = прозрачно) + toolBlockOpacity: 55, // % — плотность фона tool-блоков в чате + toolBlockBlur: 0, // px — стекло tool-блоков в чате + rgbUsername: true, // RGB-переливание ника пользователя (по умолчанию вкл) dangerousPatterns: DEFAULT_DANGEROUS_PATTERNS, // список regex-паттернов опасных команд - language: 'ru', // язык UI: 'ru' | 'en' + language: "ru", // язык UI: 'ru' | 'en' formattersEnabled: true, // авто-форматирование после write/edit (prettier/gofmt/ruff/...) // ===== Панель Cookie Code ===== - overlayOpacity: 72, // % — плотность фона панели оверлея - overlayBlur: 12, // px — размытие стекла панели - overlayWidth: 300, // px — ширина панели - overlayBgColor: '#111322', // цвет подложки панели - overlayPrimaryColor: '#8b93ff', // основной цвет кнопок панели - overlayBtnRadius: 10, // px — скругление кнопок панели + overlayOpacity: 72, // % — плотность фона панели оверлея + overlayBlur: 12, // px — размытие стекла панели + overlayWidth: 300, // px — ширина панели + overlayBgColor: "#111322", // цвет подложки панели + overlayPrimaryColor: "#8b93ff", // основной цвет кнопок панели + overlayBtnRadius: 10, // px — скругление кнопок панели // ===== Telegram-бот (botsrc/) ===== - telegramEnabled: false, // включить бота (polling + уведомления) - telegramBotToken: '', // токен от @BotFather - telegramChatId: '', // id твоего чата с ботом + telegramEnabled: false, // включить бота (polling + уведомления) + telegramBotToken: "", // токен от @BotFather + telegramLanguage: "ru", // язык бота: 'ru' | 'en' (дефолт — системный) + telegramChatId: "", // id твоего чата с ботом telegramNotifyTools: false, // присылать уведомления о результате tool - telegramChatFeed: false, // принимать сообщения из TG в чат DeepSeek + telegramChatFeed: false, // принимать сообщения из TG в чат DeepSeek // ===== Подтверждение tool-вызовов (approval gate) ===== // 'off' — выполнять всё автоматически (прежнее поведение) // 'risky' — спрашивать подтверждение только для рискованных инструментов // (bash/pwsh/write/edit/mysql/inject_js/...) // 'all' — спрашивать подтверждение для каждого tool-вызова и JS-блока - toolApprovalMode: 'off', + toolApprovalMode: "off", // Скрывать служебные сообщения в чате (результаты инструментов, JS-сводки, // системный промпт) — они по-прежнему уходят в AI, но не показываются в UI. hideSystemMessages: false, @@ -71,7 +72,7 @@ let cachedPath = null; function getSettingsPath() { if (!cachedPath) { - cachedPath = path.join(app.getPath('userData'), 'cuckoo-settings.json'); + cachedPath = path.join(app.getPath("userData"), "cuckoo-settings.json"); } return cachedPath; } @@ -82,28 +83,43 @@ function getSettingsPath() { * Используется как дефолт, если пользователь ещё не выбрал язык явно. */ function getSystemLanguage() { - let locale = ''; + let locale = ""; try { - locale = (app.getLocale && app.getLocale()) || ''; - } catch (_) { /* app может быть недоступен в тестах */ } + locale = (app.getLocale && app.getLocale()) || ""; + } catch (_) { + /* app может быть недоступен в тестах */ + } const lang = String(locale).toLowerCase().split(/[-_]/)[0]; - const RU_LIKE = ['ru', 'uk', 'be', 'kk', 'ky', 'uz', 'tg', 'hy', 'az', 'mo']; - return RU_LIKE.includes(lang) ? 'ru' : 'en'; + const RU_LIKE = ["ru", "uk", "be", "kk", "ky", "uz", "tg", "hy", "az", "mo"]; + return RU_LIKE.includes(lang) ? "ru" : "en"; } function readSettings() { try { const file = getSettingsPath(); - if (!fs.existsSync(file)) return { ...DEFAULTS, language: getSystemLanguage() }; - const raw = fs.readFileSync(file, 'utf-8'); + if (!fs.existsSync(file)) + return { + ...DEFAULTS, + language: getSystemLanguage(), + telegramLanguage: getSystemLanguage(), + }; + const raw = fs.readFileSync(file, "utf-8"); const parsed = JSON.parse(raw); const merged = { ...DEFAULTS, ...parsed }; // Пользователь ещё не выбирал язык — подтягиваем язык системы. - if (!parsed || parsed.language == null) merged.language = getSystemLanguage(); + if (!parsed || parsed.language == null) + merged.language = getSystemLanguage(); + // То же для языка Telegram-бота. + if (!parsed || parsed.telegramLanguage == null) + merged.telegramLanguage = getSystemLanguage(); return merged; } catch (err) { - console.error('[Cookie Code] 读取 settings.json 失败:', err.message); - return { ...DEFAULTS, language: getSystemLanguage() }; + console.error("[Cookie Code] 读取 settings.json 失败:", err.message); + return { + ...DEFAULTS, + language: getSystemLanguage(), + telegramLanguage: getSystemLanguage(), + }; } } @@ -111,11 +127,11 @@ function writeSettings(settings) { try { const file = getSettingsPath(); const merged = { ...DEFAULTS, ...settings }; - fs.writeFileSync(file, JSON.stringify(merged, null, 2), 'utf-8'); - console.log('[Cookie Code] settings.json 已保存:', file); + fs.writeFileSync(file, JSON.stringify(merged, null, 2), "utf-8"); + console.log("[Cookie Code] settings.json 已保存:", file); return merged; } catch (err) { - console.error('[Cookie Code] 写入 settings.json 失败:', err.message); + console.error("[Cookie Code] 写入 settings.json 失败:", err.message); return null; } } From 6d3c6ab0b382e57a41b93fb49a37128292cac4c4 Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Thu, 17 Sep 2026 14:00:12 +0300 Subject: [PATCH 2/4] Telegram: sessions, pagination & attachments Enhance Telegram integration: add session management (/sessions, /switch), commit browsing (/log, /show, /files), paginated views for /diff, /todos and other lists with inline keyboard navigation, and attachments support (download from TG and inject into chat). Add per-user restriction (telegramAllowedUserId) and tool notification ignore list. Implement helper pager/rendering, temp download dir, file/photo attach flows, telegram API helpers (downloadFile, setMyCommands), and register bot commands. Update i18n strings and settings UI/defaults accordingly. --- botsrc/i18n.js | 98 +++++- botsrc/index.js | 628 +++++++++++++++++++++++++++++++++++-- botsrc/telegram.js | 67 +++- src/main/settings-store.js | 4 + src/preload/i18n/i18n.js | 8 + 5 files changed, 769 insertions(+), 36 deletions(-) diff --git a/botsrc/i18n.js b/botsrc/i18n.js index 34ff6d0..40139c7 100644 --- a/botsrc/i18n.js +++ b/botsrc/i18n.js @@ -38,6 +38,11 @@ const STRINGS = { "help.cmd.new": "/new — новый чат", "help.cmd.diff": "/diff — показать изменения (git diff)", "help.cmd.diagnostics": "/diagnostics — диагностика интеграции", + "help.cmd.sessions": "/sessions — список сессий проекта", + "help.cmd.switch": "/switch <id> — переключиться на сессию", + "help.cmd.log": "/log — история коммитов (git log)", + "help.cmd.show": "/show <hash> — diff коммита", + "help.cmd.files": "/files <hash> — файлы коммита", "help.cmd.todos": "/todos — список задач активного окна", "help.cmd.cancel": "/cancel — отменить ввод / подтверждение", "help.cmd.help": "/help — эта справка", @@ -98,6 +103,8 @@ const STRINGS = { "label.telegramChatFeed": "Принимать из TG", "label.telegramBotToken": "Токен бота", "label.telegramChatId": "Chat ID", + "label.telegramAllowedUserId": "Разрешённый User ID", + "label.telegramToolNotifyIgnore": "Исключить tool из уведомлений", // ===== Ввод значения настройки ===== "input.prompt": "Отправьте новое значение", @@ -154,6 +161,44 @@ const STRINGS = { "todos.empty": "☑ Список задач пуст.", "todos.header": "☑ Задачи ({done}/{total}):", + // ===== /sessions и /switch ===== + "sessions.empty": "📭 Сессий не найдено.", + "sessions.title": "🗂 Сессии (стр. {page}/{pages})", + "sessions.hint": "Переключиться: /switch <id>", + "sessions.switched": "✅ Переключено на сессию: {id}", + "sessions.switchFailed": "⚠️ Не удалось переключиться: {err}", + "sessions.needId": "⚠️ Укажите id сессии: /switch <id>", + + // ===== /log, /show, /files ===== + "log.empty": "📭 Коммитов не найдено.", + "log.title": "📜 История коммитов (стр. {page}/{pages})", + "log.line": "{short} · {date} · {subject}", + "log.hint": "Diff: /show <hash> · Файлы: /files <hash>", + "show.needHash": "⚠️ Укажите хеш коммита: /show <hash>", + "show.title": "📄 Коммит {hash}", + "show.unavailable": "(diff недоступен)", + "files.needHash": "⚠️ Укажите хеш коммита: /files <hash>", + "files.title": "📁 Файлы коммита {hash}", + "files.empty": "Файлов нет.", + "files.status.added": "➕", + "files.status.modified": "✏️", + "files.status.deleted": "➖", + "files.status.renamed": "➡️", + "files.status.changed": "•", + + // ===== Вложения из Telegram ===== + "attach.saved": "📎 Файл получен: {name}", + "attach.attaching": "⏳ Прикрепляю к чату…", + "attach.done": "✅ Прикреплено к чату: {name}", + "attach.failed": "⚠️ Не удалось прикрепить: {err}", + "attach.downloading": "⏳ Скачиваю вложение…", + "attach.tooBig": "⚠️ Файл слишком большой (макс. {max}MB).", + "attach.unsupported": "⚠️ Пока поддерживаются только фото и документы.", + + // ===== Пагинация ===== + "page.prev": "◀️ Назад", + "page.next": "Вперёд ▶️", + // ===== Уведомления о tool ===== "tool.editTitle": " ", "tool.editEmpty": "(пустое изменение)", @@ -223,6 +268,11 @@ const STRINGS = { "help.cmd.new": "/new — new chat", "help.cmd.diff": "/diff — show changes (git diff)", "help.cmd.diagnostics": "/diagnostics — integration diagnostics", + "help.cmd.sessions": "/sessions — project sessions list", + "help.cmd.switch": "/switch <id> — switch to a session", + "help.cmd.log": "/log — commit history (git log)", + "help.cmd.show": "/show <hash> — commit diff", + "help.cmd.files": "/files <hash> — commit files", "help.cmd.todos": "/todos — tasks of the active window", "help.cmd.cancel": "/cancel — cancel input / approval", "help.cmd.help": "/help — this help", @@ -237,17 +287,17 @@ const STRINGS = { "settings.root.title": "⚙️ Cookie Code settings", "settings.root.text": "What to configure?", "settings.page.ui": "🎨 Interface", - "settings.page.glass": "🪟 Glass & panel", - "settings.page.agent": "🤖 Agent & privacy", + "settings.page.glass": "🪟 Glass & panel", + "settings.page.agent": "🤖 Agent & privacy", "settings.page.tg": "📡 Telegram bot", "settings.page.lang": "🌐 Language", "settings.ui.title": "🎨 Interface", "settings.ui.text": "General visual effects.", - "settings.glass.title": "🪟 Glass & panel", + "settings.glass.title": "🪟 Glass & panel", "settings.glass.text": "Transparency, blur and colors of the Cookie Code panel.", - "settings.agent.title": "🤖 Agent & privacy", + "settings.agent.title": "🤖 Agent & privacy", "settings.agent.text": "Approvals, system messages, formatters.", "settings.tg.title": "📡 Telegram bot", "settings.tg.text": "Bot control and notifications.", @@ -284,6 +334,8 @@ const STRINGS = { "label.telegramChatFeed": "Receive from TG", "label.telegramBotToken": "Bot token", "label.telegramChatId": "Chat ID", + "label.telegramAllowedUserId": "Allowed User ID", + "label.telegramToolNotifyIgnore": "Tools excluded from notifications", // ===== Setting input ===== "input.prompt": "Send a new value", @@ -339,6 +391,44 @@ const STRINGS = { "todos.empty": "☑ Task list is empty.", "todos.header": "☑ Tasks ({done}/{total}):", + // ===== /sessions and /switch ===== + "sessions.empty": "📭 No sessions found.", + "sessions.title": "🗂 Sessions (page {page}/{pages})", + "sessions.hint": "Switch: /switch <id>", + "sessions.switched": "✅ Switched to session: {id}", + "sessions.switchFailed": "⚠️ Failed to switch: {err}", + "sessions.needId": "⚠️ Provide a session id: /switch <id>", + + // ===== /log, /show, /files ===== + "log.empty": "📭 No commits found.", + "log.title": "📜 Commit history (page {page}/{pages})", + "log.line": "{short} · {date} · {subject}", + "log.hint": "Diff: /show <hash> · Files: /files <hash>", + "show.needHash": "⚠️ Provide a commit hash: /show <hash>", + "show.title": "📄 Commit {hash}", + "show.unavailable": "(diff unavailable)", + "files.needHash": "⚠️ Provide a commit hash: /files <hash>", + "files.title": "📁 Files in commit {hash}", + "files.empty": "No files.", + "files.status.added": "➕", + "files.status.modified": "✏️", + "files.status.deleted": "➖", + "files.status.renamed": "➡️", + "files.status.changed": "•", + + // ===== Attachments from Telegram ===== + "attach.saved": "📎 File received: {name}", + "attach.attaching": "⏳ Attaching to chat…", + "attach.done": "✅ Attached to chat: {name}", + "attach.failed": "⚠️ Failed to attach: {err}", + "attach.downloading": "⏳ Downloading attachment…", + "attach.tooBig": "⚠️ File too large (max {max}MB).", + "attach.unsupported": "⚠️ Only photos and documents are supported yet.", + + // ===== Pagination ===== + "page.prev": "◀️ Back", + "page.next": "Next ▶️", + // ===== Tool notifications ===== "tool.editTitle": " ", "tool.editEmpty": "(empty change)", diff --git a/botsrc/index.js b/botsrc/index.js index 086bb5c..89f2773 100644 --- a/botsrc/index.js +++ b/botsrc/index.js @@ -51,6 +51,10 @@ function _read() { token: s.telegramBotToken || "", chatId: s.telegramChatId || "", notifyTools: !!s.telegramNotifyTools, + notifyIgnore: Array.isArray(s.telegramToolNotifyIgnore) + ? s.telegramToolNotifyIgnore.map((x) => String(x).trim().toLowerCase()) + : [], + allowedUserId: String(s.telegramAllowedUserId || "").trim(), chatFeed: !!s.telegramChatFeed, approvalMode: s.toolApprovalMode || "off", lang: i18n.normalizeLang(s.telegramLanguage), @@ -722,6 +726,16 @@ const SETTINGS_PAGES = { secret: true, }, { key: "telegramChatId", labelKey: "label.telegramChatId", type: "text" }, + { + key: "telegramAllowedUserId", + labelKey: "label.telegramAllowedUserId", + type: "text", + }, + { + key: "telegramToolNotifyIgnore", + labelKey: "label.telegramToolNotifyIgnore", + type: "multiline", + }, ], back: true, }, @@ -898,6 +912,28 @@ async function _handleSettingsCallback(data, cbq) { const msgId = cbq.message && cbq.message.message_id; const state = _settingsState.get(chatId) || { page: "root" }; + // ---- Пагинация списков (/sessions, /log) ---- + if (data.startsWith("st_pg_")) { + const rest = data.slice("st_pg_".length); + const sep = rest.lastIndexOf("_"); + const type = rest.slice(0, sep); + const page = parseInt(rest.slice(sep + 1), 10) || 1; + await telegramBot.answerCallbackQuery(cbq.id); + const pager = _pagers.get(chatId + ":" + type); + if (!pager) return; + pager.page = page; + if (type === "sessions") { + await _cmdSessions(chatId, page); + } else if (type === "log") { + await _cmdLog(chatId, page); + } else if (type === "todos") { + await _cmdTodos(chatId, page); + } else if (type === "diff") { + await _cmdDiff(chatId, page); + } + return; + } + // ---- Выбор языка бота (/start) ---- if (data.startsWith("st_lang_")) { const lang = i18n.normalizeLang(data.slice("st_lang_".length)); @@ -1142,11 +1178,9 @@ async function _cmdNew() { return telegramBot.sendMessage(_t("new.done")); } -/** /diff — показать текущие изменения (git diff) в проекте. */ -async function _cmdDiff() { - const ctx = _activeContext(); - const projectDir = - ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; +/** /diff [page] — показать текущие изменения (git diff) в проекте (с пагинацией). */ +async function _cmdDiff(chatId, page) { + const projectDir = _projectDir(); if (!projectDir) return telegramBot.sendMessage(_t("diff.noProject")); const gitDiff = require("../src/main/git-diff"); @@ -1160,10 +1194,18 @@ async function _cmdDiff() { const files = status.files || []; if (files.length === 0) return telegramBot.sendMessage(_t("diff.none")); + const pager = { items: files, page: page || 1, pageSize: 10 }; + if (chatId) _pagers.set(chatId + ":diff", pager); + const size = pager.pageSize; + const pages = Math.max(1, Math.ceil(files.length / size)); + let cur = Math.min(Math.max(1, pager.page), pages); + pager.page = cur; + const slice = files.slice((cur - 1) * size, cur * size); + const chunks = []; let total = 0; const MAX_TOTAL = 3000; - for (const f of files) { + for (const f of slice) { if (total >= MAX_TOTAL) break; const d = await gitDiff.getFileDiff(projectDir, f.path, f.status); if (!d.success || !d.diff) continue; @@ -1172,8 +1214,9 @@ async function _cmdDiff() { total += text.length; } - const header = _t("diff.header", { n: files.length }) + "\n"; - const fileList = files.map((f) => "• " + escapeHtml(f.path)).join("\n"); + const header = + _t("diff.header", { n: files.length }) + " (" + cur + "/" + pages + ")\n"; + const fileList = slice.map((f) => "• " + escapeHtml(f.path)).join("\n"); const body = chunks.join("\n\n") || _t("diff.unavailable"); const msg = header + @@ -1182,7 +1225,23 @@ async function _cmdDiff() { "
\n
" +
     escapeHtml(body) +
     "
"; - return telegramBot.sendMessage(msg, { parseMode: "HTML" }); + const keyboard = []; + const nav = []; + if (cur > 1) + nav.push({ + text: _t("page.prev"), + callback_data: "st_pg_diff_" + (cur - 1), + }); + if (cur < pages) + nav.push({ + text: _t("page.next"), + callback_data: "st_pg_diff_" + (cur + 1), + }); + if (nav.length) keyboard.push(nav); + return telegramBot.sendMessage(msg, { + parseMode: "HTML", + replyMarkup: { inline_keyboard: keyboard }, + }); } /** /diagnostics — отчёт диагностики интеграции. */ @@ -1199,6 +1258,232 @@ async function _cmdDiagnostics() { ); } +/** /todos — список задач активного окна (с пагинацией). */ +async function _cmdTodos(chatId, page) { + let todos = []; + try { + const senderId = _activeSenderId(); + if (senderId != null) { + const todoStore = require("../src/main/todo-store"); + todos = todoStore.getList(senderId); + } + } catch (err) { + _log("error", "/todos error:", err.message); + } + if (!Array.isArray(todos) || todos.length === 0) { + return telegramBot.sendMessage(_t("todos.empty")); + } + const icon = { pending: "☐", in_progress: "◔", completed: "☑" }; + const pager = { items: todos, page: page || 1, pageSize: 10 }; + _pagers.set(chatId + ":todos", pager); + const done = todos.filter((t) => t.status === "completed").length; + const rendered = _renderPager( + "todos", + pager, + (t) => (icon[t.status] || "☐") + " " + escapeHtml(t.content), + (p, pages) => + _t("todos.header", { done: done, total: todos.length }) + + " (" + + p + + "/" + + pages + + ")", + "", + ); + return telegramBot.sendMessage(rendered.text, { + parseMode: "HTML", + replyMarkup: rendered.keyboard, + }); +} + +/** Текущий projectDir активного окна. */ +function _projectDir() { + const ctx = _activeContext(); + return ( + (ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir) || + null + ); +} + +// Пагинация: key = `${chatId}:${type}` → { items, page, pageSize } +const _pagers = new Map(); + +/** Отрисовать страницу списка с навигацией. */ +function _renderPager(type, pager, renderItem, titleFn, hint) { + const size = pager.pageSize || 10; + const total = pager.items.length; + const pages = Math.max(1, Math.ceil(total / size)); + let page = Math.min(Math.max(1, pager.page), pages); + pager.page = page; + const slice = pager.items.slice((page - 1) * size, page * size); + const lines = [titleFn(page, pages)]; + if (hint) lines.push(hint); + lines.push(""); + for (const it of slice) lines.push(renderItem(it)); + const keyboard = []; + const nav = []; + if (page > 1) + nav.push({ + text: _t("page.prev"), + callback_data: "st_pg_" + type + "_" + (page - 1), + }); + if (page < pages) + nav.push({ + text: _t("page.next"), + callback_data: "st_pg_" + type + "_" + (page + 1), + }); + if (nav.length) keyboard.push(nav); + return { text: lines.join("\n"), keyboard: { inline_keyboard: keyboard } }; +} + +/** /sessions — список сессий проекта. */ +async function _cmdSessions(chatId, page) { + const projectDir = _projectDir(); + if (!projectDir) return telegramBot.sendMessage(_t("diff.noProject")); + let sessions = []; + try { + const ctx = _activeContext(); + const store = ctx && ctx.sessionStore; + const all = store.readSessionStore(); + sessions = Object.keys(all).filter((id) => all[id] === projectDir); + } catch (err) { + return telegramBot.sendMessage("⚠ " + escapeHtml(err.message)); + } + if (sessions.length === 0) + return telegramBot.sendMessage(_t("sessions.empty")); + const pager = { items: sessions, page: page || 1, pageSize: 10 }; + _pagers.set(chatId + ":sessions", pager); + const rendered = _renderPager( + "sessions", + pager, + (id) => "• " + escapeHtml(id) + "", + (p, pages) => _t("sessions.title", { page: p, pages: pages }), + _t("sessions.hint"), + ); + return telegramBot.sendMessage(rendered.text, { + parseMode: "HTML", + replyMarkup: rendered.keyboard, + }); +} + +/** /switch — переключиться на сессию. */ +async function _cmdSwitch(chatId, id) { + if (!id) + return telegramBot.sendMessage(_t("sessions.needId"), { + parseMode: "HTML", + }); + const res = await _evalInWindow( + "(async () => { try { return await window.electronAPI.navigateSession(" + + JSON.stringify(id) + + "); } catch (e) { return { success:false, error:e.message }; } })()", + ); + const inner = (res && res.result) || {}; + if (!res.success || inner.success === false) + return telegramBot.sendMessage( + _t("sessions.switchFailed", { + err: escapeHtml( + (inner && inner.error) || (res && res.error) || "unknown", + ), + }), + ); + return telegramBot.sendMessage( + _t("sessions.switched", { id: escapeHtml(id) }), + { parseMode: "HTML" }, + ); +} + +/** /log — история коммитов. */ +async function _cmdLog(chatId, page) { + const projectDir = _projectDir(); + if (!projectDir) return telegramBot.sendMessage(_t("diff.noProject")); + const gitDiff = require("../src/main/git-diff"); + const r = await gitDiff.getLog(projectDir, 100); + if (!r.success) + return telegramBot.sendMessage( + _t("diff.gitUnavailable", { + reason: escapeHtml(r.reason || _t("diff.gitDefault")), + }), + ); + const commits = r.commits || []; + if (commits.length === 0) return telegramBot.sendMessage(_t("log.empty")); + const pager = { items: commits, page: page || 1, pageSize: 10 }; + _pagers.set(chatId + ":log", pager); + const rendered = _renderPager( + "log", + pager, + (c) => + _t("log.line", { + short: escapeHtml(c.short), + date: escapeHtml(String(c.date || "").slice(0, 16)), + subject: escapeHtml(c.subject), + }), + (p, pages) => _t("log.title", { page: p, pages: pages }), + _t("log.hint"), + ); + return telegramBot.sendMessage(rendered.text, { + parseMode: "HTML", + replyMarkup: rendered.keyboard, + }); +} + +/** /show — diff коммита. */ +async function _cmdShow(chatId, hash) { + if (!hash) + return telegramBot.sendMessage(_t("show.needHash"), { + parseMode: "HTML", + }); + const projectDir = _projectDir(); + if (!projectDir) return telegramBot.sendMessage(_t("diff.noProject")); + const gitDiff = require("../src/main/git-diff"); + const r = await gitDiff.getCommitDiff(projectDir, hash); + if (!r.success) + return telegramBot.sendMessage( + _t("diff.gitUnavailable", { + reason: escapeHtml(r.reason || _t("diff.gitDefault")), + }), + ); + const body = String(r.diff || "").slice(0, 3500) || _t("show.unavailable"); + const msg = + _t("show.title", { hash: escapeHtml(hash) }) + + "\n
" +
+    escapeHtml(body) +
+    "
"; + return telegramBot.sendMessage(msg, { parseMode: "HTML" }); +} + +/** /files — файлы коммита. */ +async function _cmdFiles(chatId, hash) { + if (!hash) + return telegramBot.sendMessage(_t("files.needHash"), { + parseMode: "HTML", + }); + const projectDir = _projectDir(); + if (!projectDir) return telegramBot.sendMessage(_t("diff.noProject")); + const gitDiff = require("../src/main/git-diff"); + const r = await gitDiff.getCommitFiles(projectDir, hash); + if (!r.success) + return telegramBot.sendMessage( + _t("diff.gitUnavailable", { + reason: escapeHtml(r.reason || _t("diff.gitDefault")), + }), + ); + const files = r.files || []; + if (files.length === 0) return telegramBot.sendMessage(_t("files.empty")); + const icon = { + added: _t("files.status.added"), + modified: _t("files.status.modified"), + deleted: _t("files.status.deleted"), + renamed: _t("files.status.renamed"), + changed: _t("files.status.changed"), + }; + const lines = files.map( + (f) => (icon[f.status] || "•") + " " + escapeHtml(f.path), + ); + const msg = + _t("files.title", { hash: escapeHtml(hash) }) + "\n" + lines.join("\n"); + return telegramBot.sendMessage(msg, { parseMode: "HTML" }); +} + async function _cmdHelp() { const text = [ _t("help.title"), @@ -1210,6 +1495,11 @@ async function _cmdHelp() { _t("help.cmd.new"), _t("help.cmd.diff"), _t("help.cmd.diagnostics"), + _t("help.cmd.sessions"), + _t("help.cmd.switch"), + _t("help.cmd.log"), + _t("help.cmd.show"), + _t("help.cmd.files"), _t("help.cmd.todos"), _t("help.cmd.cancel"), _t("help.cmd.help"), @@ -1279,7 +1569,8 @@ async function _handleSettingsInput(chatId, text) { ok && (key === "telegramBotToken" || key === "telegramChatId" || - key === "telegramEnabled") + key === "telegramEnabled" || + key === "telegramAllowedUserId") ) { try { await applySettings(); @@ -1297,11 +1588,225 @@ async function _handleSettingsInput(chatId, text) { return true; } +/** Папка для временных вложений из Telegram (userData/tgtemp). */ +function _tgTempDir() { + try { + const { app } = require("electron"); + return require("path").join(app.getPath("userData"), "tgtemp"); + } catch (_) { + return require("path").join(__dirname, "tgtemp"); + } +} + +/** + * Отправить сообщение из поля ввода активного окна. + * Приоритет: клик по кнопке отправки → нативный Enter (как chat-send-enter). + */ +async function _sendInWindow(win) { + try { + const wc = win.webContents; + const clickScript = `(function(){ + const sels = ['button[type="submit"]','button[aria-label*="send"]','button[aria-label*="发送"]','button[data-testid="send-button"]','button[data-testid="send"]','.send-btn','.submit-btn']; + for (const s of sels) { + try { const b = document.querySelector(s); if (b && !b.disabled && b.offsetParent !== null) { b.click(); return true; } } catch (_) {} + } + return false; + })()`; + // 1) Пытаемся кликнуть кнопку отправки (до ~3 c, она может стать + // активной не сразу после вставки вложения). + for (let i = 0; i < 10; i++) { + let clicked = false; + try { + clicked = await wc.executeJavaScript(clickScript, true); + } catch (_) {} + if (clicked) return { success: true, via: "button" }; + await new Promise((r) => setTimeout(r, 300)); + } + // 2) Нативный Enter (keyCode Enter — как в chat-send-enter). + wc.sendInputEvent({ type: "keyDown", keyCode: "Enter" }); + wc.sendInputEvent({ type: "char", keyCode: "Enter", key: "\r" }); + wc.sendInputEvent({ type: "keyUp", keyCode: "Enter" }); + return { success: true, via: "enter" }; + } catch (err) { + return { success: false, error: err.message }; + } +} + +/** + * Вставить изображение в поле чата (clipboard + Ctrl+V) — надёжный путь как у read_photo. + */ +async function _insertImageToWindow(absPath, caption, send) { + try { + const { clipboard, nativeImage } = require("electron"); + const win = windowState.getMainWindow(); + if (!win || win.isDestroyed()) + return { success: false, error: _t("common.noWindow") }; + const img = nativeImage.createFromPath(absPath); + if (img.isEmpty()) return { success: false, error: "empty image" }; + clipboard.writeImage(img); + const safe = JSON.stringify(String(caption || "")); + const script = `(function(){ + const ta = document.querySelector('textarea[placeholder], textarea[name="search"], textarea.ds-scroll-area'); + if (!ta) return { ok: false, error: 'textarea not found' }; + const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set; + setter.call(ta, ${safe}); + ta.dispatchEvent(new Event('input', { bubbles: true })); + ta.focus(); + return { ok: true }; + })()`; + await win.webContents.executeJavaScript(script, true); + await new Promise((r) => setTimeout(r, 150)); + win.webContents.sendInputEvent({ + type: "keyDown", + keyCode: "V", + modifiers: ["control"], + }); + win.webContents.sendInputEvent({ + type: "keyUp", + keyCode: "V", + modifiers: ["control"], + }); + await new Promise((r) => setTimeout(r, 500)); + if (send) { + await new Promise((r) => setTimeout(r, 300)); + await _sendInWindow(win); + } + return { success: true }; + } catch (err) { + return { success: false, error: err.message }; + } +} + +/** + * Прикрепить произвольный файл к чату через скрытый . + */ +async function _attachFileToWindow(absPath, caption, send) { + try { + const fs = require("fs"); + const path = require("path"); + const { + guessMimeType, + buildInjectCode, + UPLOAD_TIMEOUT_MS, + MAX_FILE_SIZE, + } = require("../tools/AttachFileTool"); + const win = windowState.getMainWindow(); + if (!win || win.isDestroyed()) + return { success: false, error: _t("common.noWindow") }; + const stat = fs.statSync(absPath); + if (stat.size > MAX_FILE_SIZE) return { success: false, error: "too big" }; + const buf = fs.readFileSync(absPath); + const fileName = path.basename(absPath); + const mimeType = guessMimeType(fileName); + const code = buildInjectCode( + buf.toString("base64"), + fileName, + mimeType, + UPLOAD_TIMEOUT_MS, + ); + const result = await win.webContents.executeJavaScript(code, true); + if (!result || result.success !== true) + return { + success: false, + error: (result && result.error) || "attach failed", + }; + if (caption || send) { + const safe = JSON.stringify(String(caption || "")); + const script = `(function(){ + const ta = document.querySelector('textarea[placeholder], textarea[name="search"], textarea.ds-scroll-area'); + if (!ta) return { ok: false }; + const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set; + setter.call(ta, ${safe}); + ta.dispatchEvent(new Event('input', { bubbles: true })); + ta.focus(); + return { ok: true }; + })()`; + await win.webContents.executeJavaScript(script, true); + await new Promise((r) => setTimeout(r, 300)); + if (send) await _sendInWindow(win); + } + return { success: true, fileName: result.fileName || fileName }; + } catch (err) { + return { success: false, error: err.message }; + } +} + +/** Обработать вложение из Telegram: скачать → /tgtemp → прикрепить → удалить. */ +async function _handleAttachment(chatId, msg) { + const { telegramBot } = require("./telegram"); + const fs = require("fs"); + const path = require("path"); + const caption = String((msg && msg.caption) || "").trim(); + let fileId = null; + let name = ""; + let isImage = false; + if (msg.photo && msg.photo.length) { + const p = msg.photo[msg.photo.length - 1]; + fileId = p.file_id; + name = "photo_" + Date.now() + ".jpg"; + isImage = true; + } else if (msg.document) { + fileId = msg.document.file_id; + name = msg.document.file_name || "document_" + Date.now(); + const mime = String(msg.document.mime_type || ""); + isImage = mime.startsWith("image/"); + } + if (!fileId) { + await telegramBot.sendMessage(_t("attach.unsupported")); + return; + } + await telegramBot.sendMessage(_t("attach.downloading")); + const dest = path.join(_tgTempDir(), name); + const dl = await telegramBot.downloadFile(fileId, dest); + if (!dl.success) { + await telegramBot.sendMessage( + _t("attach.failed", { err: escapeHtml(dl.error) }), + ); + return; + } + await telegramBot.sendMessage( + _t("attach.saved", { name: escapeHtml(name) }), + { parseMode: "HTML" }, + ); + const at = isImage + ? await _insertImageToWindow(dest, caption, true) + : await _attachFileToWindow(dest, caption, true); + if (at.success) { + await telegramBot.sendMessage( + _t("attach.done", { name: escapeHtml(name) }), + { parseMode: "HTML" }, + ); + } else { + await telegramBot.sendMessage( + _t("attach.failed", { err: escapeHtml(at.error || "unknown") }), + { parseMode: "HTML" }, + ); + } + try { + fs.unlinkSync(dest); + } catch (_) {} +} + /** Входящее из TG → в чат DeepSeek (или команда). */ -async function _handleIncoming(chatId, text) { +async function _handleIncoming(chatId, text, msg) { const cfg = _read(); const raw = String(text || ""); - const cmd = raw.trim().toLowerCase(); + const trimmed = raw.trim(); + // Команда = первое слово; убираем суффикс @BotName (Telegram добавляет его + // в группах) и приводим к нижнему регистру. + const cmd = (trimmed.split(/\s+/)[0] || "") + .toLowerCase() + .replace(/@[a-z0-9_]+$/, ""); + // Аргументы команды (всё после первого слова). + const cmdArgs = trimmed.split(/\s+/).slice(1).join(" ").trim(); + + // ---- Вложения (фото/документы) ---- + // Подпись (caption) обрабатывается внутри _handleAttachment, поэтому + // после вложения дальше не идём (иначе caption уйдёт вторым сообщением). + if (msg && (msg.photo || msg.document)) { + await _handleAttachment(chatId, msg); + return; + } // ---- Служебные команды ---- if (cmd === "/start") { @@ -1334,13 +1839,33 @@ async function _handleIncoming(chatId, text) { return; } if (cmd === "/diff") { - await _cmdDiff(); + await _cmdDiff(chatId, 1); return; } if (cmd === "/diagnostics" || cmd === "/diag") { await _cmdDiagnostics(); return; } + if (cmd === "/sessions") { + await _cmdSessions(chatId, 1); + return; + } + if (cmd === "/switch") { + await _cmdSwitch(chatId, cmdArgs); + return; + } + if (cmd === "/log") { + await _cmdLog(chatId, 1); + return; + } + if (cmd === "/show") { + await _cmdShow(chatId, cmdArgs); + return; + } + if (cmd === "/files") { + await _cmdFiles(chatId, cmdArgs); + return; + } if (cmd === "/cancel") { const hadSettings = _settingsWaiting.delete(chatId); // Отменяем все ожидающие approval (AI больше не ждёт подтверждения). @@ -1359,17 +1884,7 @@ async function _handleIncoming(chatId, text) { } // Команда /todos — показать список задач активного окна. if (cmd === "/todos" || cmd === "/todo") { - let todos = []; - try { - const senderId = _activeSenderId(); - if (senderId != null) { - const todoStore = require("../src/main/todo-store"); - todos = todoStore.getList(senderId); - } - } catch (err) { - _log("error", "/todos error:", err.message); - } - await telegramBot.sendMessage(formatTodos(todos)); + await _cmdTodos(chatId, 1); return; } @@ -1491,6 +2006,13 @@ async function notifyToolResult(toolName, ok, detail) { const cfg = _read(); if (!cfg.enabled || !cfg.notifyTools) return { success: false, skipped: true }; + // Умные уведомления: пропускаем инструменты из списка исключений. + if ( + cfg.notifyIgnore.length > 0 && + cfg.notifyIgnore.includes(String(toolName || "").toLowerCase()) + ) { + return { success: false, skipped: true }; + } const emoji = ok ? "✅" : "❌"; const header = emoji + " " + escapeHtml(toolName); @@ -1554,11 +2076,13 @@ async function notifyToolResult(toolName, ok, detail) { /** Применить настройки: пересоздать конфиг бота и (при необходимости) запустить polling. */ async function applySettings() { const cfg = _read(); - telegramBot.configure(cfg.token, cfg.chatId); + telegramBot.configure(cfg.token, cfg.chatId, cfg.allowedUserId); if (cfg.enabled && cfg.token) { telegramBot.startPolling(_handleIncoming, _handleCallback); started = true; + // Меню команд в Telegram ("/"). + _registerCommands().catch(() => {}); } else { telegramBot.stopPolling(); started = false; @@ -1570,6 +2094,54 @@ function getStatus() { return Object.assign({ started }, telegramBot.getStatus()); } +/** Зарегистрировать список команд бота (меню "/" в Telegram) на текущем языке. */ +async function _registerCommands() { + const lang = _lang(); + const t = (k) => i18n.t(lang, k); + // Описания команд (короткие, без префикса пути). + const desc = { + ru: { + start: "начать / справка", + help: "справка", + settings: "настройки", + status: "статус", + stop: "остановить задачу", + new: "новый чат", + diff: "изменения (git diff)", + log: "история коммитов", + show: "diff коммита", + files: "файлы коммита", + sessions: "список сессий", + switch: "переключить сессию", + diagnostics: "диагностика", + todos: "список задач", + cancel: "отменить ввод", + }, + en: { + start: "start / help", + help: "help", + settings: "settings", + status: "status", + stop: "stop task", + new: "new chat", + diff: "changes (git diff)", + log: "commit history", + show: "commit diff", + files: "commit files", + sessions: "sessions list", + switch: "switch session", + diagnostics: "diagnostics", + todos: "todo list", + cancel: "cancel", + }, + }[lang]; + const cmds = Object.keys(desc).map((c) => ({ + command: c, + description: desc[c], + })); + return telegramBot.setMyCommands(cmds); +} + // ==================== «ИИ печатает…» ==================== // // Telegram показывает статус «печатает…» ~5 секунд после sendChatAction('typing'). @@ -1582,7 +2154,7 @@ function startTyping() { const cfg = _read(); if (!cfg.enabled || !cfg.token || !cfg.chatId) return { success: false, skipped: true }; - telegramBot.configure(cfg.token, cfg.chatId); + telegramBot.configure(cfg.token, cfg.chatId, cfg.allowedUserId); if (_typingTimer) return { success: true, already: true }; const tick = () => { telegramBot.sendChatAction("typing").catch(() => {}); @@ -1622,14 +2194,14 @@ async function notifyAIResponse(text) { /** Пингануть бота (getMe) для проверки токена. */ async function ping() { const cfg = _read(); - telegramBot.configure(cfg.token, cfg.chatId); + telegramBot.configure(cfg.token, cfg.chatId, cfg.allowedUserId); return telegramBot.getMe(); } /** Отправить тестовое сообщение. */ async function testSend() { const cfg = _read(); - telegramBot.configure(cfg.token, cfg.chatId); + telegramBot.configure(cfg.token, cfg.chatId, cfg.allowedUserId); return telegramBot.sendMessage(_t("test.send")); } diff --git a/botsrc/telegram.js b/botsrc/telegram.js index 881d368..6ed7e9b 100644 --- a/botsrc/telegram.js +++ b/botsrc/telegram.js @@ -20,6 +20,7 @@ class TelegramBot { constructor() { this.token = ""; this.chatId = ""; + this.allowedUserId = ""; this.polling = false; this.offset = 0; this.pollTimer = null; @@ -50,10 +51,13 @@ class TelegramBot { } catch (_) {} } - /** Настроить токен/chat_id. Возвращает true, если параметры валидны. */ - configure(token, chatId) { + /** Настроить токен/chat_id/allowedUserId. Возвращает true, если параметры валидны. */ + configure(token, chatId, allowedUserId) { this.token = String(token || "").trim(); this.chatId = String(chatId || "").trim(); + if (allowedUserId !== undefined) { + this.allowedUserId = String(allowedUserId || "").trim(); + } return this.isConfigured(); } @@ -291,6 +295,51 @@ class TelegramBot { } } + /** + * Установить список команд бота (меню "/" в Telegram). + * @param {Array<{command:string, description:string}>} commands + */ + async setMyCommands(commands) { + if (!this.token) return { success: false, error: "token не задан" }; + const list = (Array.isArray(commands) ? commands : []).filter( + (c) => c && c.command && c.description, + ); + if (list.length === 0) return { success: true, skipped: true }; + const r = await this._callApi("setMyCommands", { commands: list }); + return r.success ? { success: true } : { success: false, error: r.error }; + } + + /** + * Скачать файл по file_id в указанный путь. + * Использует getFile → https://api.telegram.org/file/bot/. + * @param {string} fileId + * @param {string} destPath — абсолютный путь для сохранения + * @returns {Promise<{success:boolean, path?:string, size?:number, error?:string}>} + */ + async downloadFile(fileId, destPath) { + if (!this.token) return { success: false, error: "token не задан" }; + if (!fileId) return { success: false, error: "file_id не задан" }; + try { + const meta = await this._callApi("getFile", { file_id: fileId }); + if (!meta.success) return { success: false, error: meta.error }; + const filePath = + meta.data && meta.data.result && meta.data.result.file_path; + if (!filePath) return { success: false, error: "file_path не получен" }; + const url = API_BASE + "/file/bot" + this.token + "/" + filePath; + const res = await fetch(url); + if (!res.ok) return { success: false, error: "HTTP " + res.status }; + const buf = Buffer.from(await res.arrayBuffer()); + const fs = require("fs"); + const path = require("path"); + const dir = path.dirname(destPath); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(destPath, buf); + return { success: true, path: destPath, size: buf.length }; + } catch (err) { + return { success: false, error: err.message }; + } + } + /** * Запустить long-polling. * @param {(chatId: string, text: string, rawMsg: object) => void} onMessage @@ -373,16 +422,26 @@ class TelegramBot { } const msg = upd.message; - if (!msg || !msg.text) continue; + if (!msg) continue; const fromChat = String(msg.chat && msg.chat.id); // Принимаем только сообщения от настроенного chat_id (если он задан). if (this.chatId && fromChat !== this.chatId) { this._log("warn", "Игнор сообщения из чужого чата:", fromChat); continue; } + // Ограничение по user id (если задан). + if (this.allowedUserId) { + const fromUser = String(msg.from && msg.from.id); + if (fromUser !== this.allowedUserId) { + this._log("warn", "Игнор сообщения от чужого user id:", fromUser); + continue; + } + } + const hasContent = !!(msg.text || msg.photo || msg.document); + if (!hasContent) continue; if (this.onMessage) { try { - this.onMessage(fromChat, msg.text, msg); + this.onMessage(fromChat, msg.text || "", msg); } catch (e) { this._log("error", "onMessage handler error:", e.message); } diff --git a/src/main/settings-store.js b/src/main/settings-store.js index 7a80ca8..04a98e1 100644 --- a/src/main/settings-store.js +++ b/src/main/settings-store.js @@ -48,7 +48,11 @@ const DEFAULTS = { telegramBotToken: "", // токен от @BotFather telegramLanguage: "ru", // язык бота: 'ru' | 'en' (дефолт — системный) telegramChatId: "", // id твоего чата с ботом + telegramAllowedUserId: "", // id пользователя TG (если задан — только он может писать) telegramNotifyTools: false, // присылать уведомления о результате tool + // Список имён tool, уведомления о которых НЕ отправлять (по одному в строке). + // Актуально только при включённом telegramNotifyTools. + telegramToolNotifyIgnore: ["read", "readLines", "glob", "grep", "todoWrite"], telegramChatFeed: false, // принимать сообщения из TG в чат DeepSeek // ===== Подтверждение tool-вызовов (approval gate) ===== // 'off' — выполнять всё автоматически (прежнее поведение) diff --git a/src/preload/i18n/i18n.js b/src/preload/i18n/i18n.js index 654cbfd..9f41611 100644 --- a/src/preload/i18n/i18n.js +++ b/src/preload/i18n/i18n.js @@ -57,6 +57,14 @@ const KEYS = { en: "Bot token (@BotFather)", }, "tg.label.chatId": { ru: "Chat ID", en: "Chat ID" }, + "tg.label.allowedUserId": { + ru: "Разрешённый User ID (пусто — любой)", + en: "Allowed User ID (empty — any)", + }, + "tg.label.notifyIgnore": { + ru: "Исключить tool из уведомлений (по одному в строке)", + en: "Exclude tools from notifications (one per line)", + }, "tg.label.enabled": { ru: "Включить бота", en: "Enable bot" }, "tg.label.notifyTools": { ru: "Уведомления о tool", From 919a4fdc9a92215df72dddf095d49b100f4a8723 Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Thu, 17 Sep 2026 14:42:43 +0300 Subject: [PATCH 3/4] Auto-version, Telegram fields, settings-tab refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docs: add and small script to auto-fetch latest GitHub release for displayed version in both en and ru pages; expand tools list, gallery markup and Telegram-related docs (language, allowed user id, notify ignore). UI: tweak text/observer options and examples. Code: large refactor of src/preload/dom/settings-tab.js — consistent quoting/formatting, safer DOM queries, improved HTML building, escape handling, and many UX fixes. Add Telegram allowedUserId and telegramToolNotifyIgnore handling (read/save/apply), improve background grid refresh/selection, slider/change handlers, diagnostics modal copy fallback, immediate apply of conv token visibility, and various console/error improvements. --- docs/en.html | 105 +- docs/index.html | 105 +- src/preload/dom/settings-tab.js | 1640 ++++++++++++++++++++----------- 3 files changed, 1276 insertions(+), 574 deletions(-) diff --git a/docs/en.html b/docs/en.html index 2ab6657..5656f08 100644 --- a/docs/en.html +++ b/docs/en.html @@ -3,6 +3,7 @@ + Cookie Code — Desktop AI agent with zero token cost
verified - Version 4.0.13 · MIT License · Free + Version … · MIT License · Free

Desktop AI agent
with @@ -307,8 +308,13 @@

Control and observe from your phone: tool-call notifications, AI - replies, the /todos and - /settings commands, incoming messages. + replies, receiving photos and documents from chat. Commands + /help, /settings, + /status, /diff, /log, + /show, /files, /sessions, + /switch, /todos, /stop. + Language selection (RU/EN), User ID filter, flexible + notifications.

@@ -481,6 +487,29 @@

Available tools

Electron browser window + JS injection + + cityTime + Current time, timezone, city and country + + + askUserQuestion + Ask the user a question with answer options + + + exitPlanMode + Finish plan mode and show the plan for approval + + + attachFile + + Attach a local file (PDF, Word, Excel, PPT, images) to the + chat input + + + + log + Output intermediate result to the execution log + @@ -506,56 +535,80 @@

Screenshots

@@ -809,7 +865,7 @@

Ready to try Cookie Code?

} }); }, - { threshold: 0.08 } + { threshold: 0.08 }, ); document.querySelectorAll(".reveal").forEach(function (el) { observer.observe(el); @@ -847,5 +903,30 @@

Ready to try Cookie Code?

}); })(); + diff --git a/docs/index.html b/docs/index.html index 491ec4d..f2b52a3 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,6 +3,7 @@ + Cookie Code — AI-агент с нулевой стоимостью токенов
verified - Версия 4.0.13 · MIT License · Бесплатно + Версия … · MIT License · Бесплатно

AI-агент на рабочем столе
с @@ -306,8 +307,12 @@

Управление и наблюдение с телефона: уведомления о вызовах, - ответы AI, команды /todos, /settings, - входящие сообщения. + ответы AI, приём фото и документов из чата. Команды + /help, /settings, + /status, /diff, /log, + /show, /files, /sessions, + /switch, /todos, /stop. + Выбор языка (RU/EN), фильтр по User ID, гибкие уведомления.

@@ -482,6 +487,29 @@

Доступные инструменты

Окно браузера Electron + инъекция JS + + cityTime + Текущее время, часовой пояс, город и страна + + + askUserQuestion + Задать пользователю вопрос с вариантами ответа + + + exitPlanMode + Завершить режим плана и показать план на утверждение + + + attachFile + + Прикрепить локальный файл (PDF, Word, Excel, PPT, + изображения) в поле ввода чата + + + + log + Вывод промежуточного результата в лог выполнения + @@ -507,56 +535,80 @@

Скриншоты

@@ -810,7 +865,7 @@

Готовы попробовать Cookie Code?

} }); }, - { threshold: 0.08 } + { threshold: 0.08 }, ); document.querySelectorAll(".reveal").forEach(function (el) { observer.observe(el); @@ -848,5 +903,31 @@

Готовы попробовать Cookie Code?

}); })(); + diff --git a/src/preload/dom/settings-tab.js b/src/preload/dom/settings-tab.js index 66d04e9..c548a33 100644 --- a/src/preload/dom/settings-tab.js +++ b/src/preload/dom/settings-tab.js @@ -13,44 +13,48 @@ * Левая панель вкладок: * .d316d158 ← контейнер кнопок вкладок */ -const background = require('./background'); -const state = require('./state'); -const { t } = require('../i18n/i18n'); +const background = require("./background"); +const state = require("./state"); +const { t } = require("../i18n/i18n"); -const TAB_BUTTON_ID = 'cuckoo-settings-tab-btn'; -const TAB_CONTENT_ID = 'cuckoo-settings-content'; +const TAB_BUTTON_ID = "cuckoo-settings-tab-btn"; +const TAB_CONTENT_ID = "cuckoo-settings-content"; -const CUCKOO_TAB_BUTTON_SELECTOR = '.d316d158'; -const MODAL_CONTENT_MAIN_SELECTOR = '.ds-modal-content__main'; -const RIGHT_PANEL_WRAPPER_SELECTOR = '.f2ff50b5'; -const NATIVE_SCROLL_AREA_SELECTOR = '.ds-scroll-area'; +const CUCKOO_TAB_BUTTON_SELECTOR = ".d316d158"; +const MODAL_CONTENT_MAIN_SELECTOR = ".ds-modal-content__main"; +const RIGHT_PANEL_WRAPPER_SELECTOR = ".f2ff50b5"; +const NATIVE_SCROLL_AREA_SELECTOR = ".ds-scroll-area"; /** * Открыть вкладку Cookie Code — скрыть родной контент и показать наш. */ function activateCuckooTab() { const nativeScroll = document.querySelector( - MODAL_CONTENT_MAIN_SELECTOR + ' ' + RIGHT_PANEL_WRAPPER_SELECTOR + ' ' + NATIVE_SCROLL_AREA_SELECTOR + MODAL_CONTENT_MAIN_SELECTOR + + " " + + RIGHT_PANEL_WRAPPER_SELECTOR + + " " + + NATIVE_SCROLL_AREA_SELECTOR, ); const wrapper = document.querySelector( - MODAL_CONTENT_MAIN_SELECTOR + ' ' + RIGHT_PANEL_WRAPPER_SELECTOR + MODAL_CONTENT_MAIN_SELECTOR + " " + RIGHT_PANEL_WRAPPER_SELECTOR, ); if (!wrapper) return; // Скрываем родной контент - if (nativeScroll) nativeScroll.style.display = 'none'; + if (nativeScroll) nativeScroll.style.display = "none"; // Вставляем свой блок, если его ещё нет let ourContent = document.getElementById(TAB_CONTENT_ID); if (!ourContent) { - ourContent = document.createElement('div'); + ourContent = document.createElement("div"); ourContent.id = TAB_CONTENT_ID; ourContent.style.cssText = - 'display: flex; flex-direction: column; gap: 16px; ' + - 'width: 100%; height: 100%; overflow-y: auto; ' + - 'padding: 20px 24px; box-sizing: border-box; ' + + "display: flex; flex-direction: column; gap: 16px; " + + "width: 100%; height: 100%; overflow-y: auto; " + + "padding: 20px 24px; box-sizing: border-box; " + 'font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; ' + - 'color: #dde1ff;'; + "color: #dde1ff;"; ourContent.innerHTML = buildContentHTML(); @@ -86,7 +90,7 @@ function activateCuckooTab() { bindAgentSettings(); refreshAgentSettings(); } - ourContent.style.display = ''; + ourContent.style.display = ""; // Подсвечиваем нашу кнопку вкладки как активную setTabActive(); @@ -96,365 +100,534 @@ function activateCuckooTab() { * HTML-содержимое вкладки настроек Cookie Code. */ function buildContentHTML() { - const items = background.getAllBackgrounds().map(backgroundItemHTML).join(''); + const items = background.getAllBackgrounds().map(backgroundItemHTML).join(""); - return '' + - '' + - '
' + + " .ck-input { width: 100%; padding: 9px 12px; background: rgba(12,14,26,0.7); color: #e8eaff; " + + " border: 1px solid rgba(255,255,255,0.1); border-radius: 10px; font-size: 12.5px; " + + " outline: none; transition: border-color 0.16s, box-shadow 0.16s; }" + + " .ck-input:focus { border-color: rgba(139,147,255,0.7); box-shadow: 0 0 0 3px rgba(139,147,255,0.18); }" + + " .ck-textarea { width: 100%; padding: 10px 12px; font-family: Consolas, monospace; font-size: 12px; " + + " background: rgba(12,14,26,0.7); color: #dde1ff; border: 1px solid rgba(255,255,255,0.1); " + + " border-radius: 10px; resize: vertical; line-height: 1.5; outline: none; transition: border-color 0.16s; }" + + " .ck-textarea:focus { border-color: rgba(139,147,255,0.7); box-shadow: 0 0 0 3px rgba(139,147,255,0.18); }" + + "" + + "
" + '
Cookie Code
' + - '
' + t('settings.subtitle') + '
' + - '
' + + '
' + + t("settings.subtitle") + + "
" + + "
" + // Кастомизация + Язык — в одной секции - '
' + - '
' + t('settings.section.customization') + '
' + + "
" + + '
' + + t("settings.section.customization") + + "
" + '
' + '
' + - '
' + - '
' + t('settings.section.customization') + '
' + - '
' + t('settings.subtitle') + '
' + - '
' + + "
" + + '
' + + t("settings.section.customization") + + "
" + + '
' + + t("settings.subtitle") + + "
" + + "
" + ' ' + - '
' + + "
" + '
' + - '
' + - '
' + t('settings.section.language') + '
' + - '
' + + "
" + + '
' + + t("settings.section.language") + + "
" + + "
" + '
' + - ' ' + - ' ' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + t('settings.section.blur') + '
' + + ' " + + ' " + + "
" + + " " + + " " + + "" + + "
" + + '
' + + t("settings.section.blur") + + "
" + '
' + - '
' + t('settings.blur.bg') + '0 px
' + + '
' + + t("settings.blur.bg") + + '0 px
' + ' ' + - '
' + + "
" + '
' + - '
' + t('settings.blur.header') + '12 px
' + + '
' + + t("settings.blur.header") + + '12 px
' + ' ' + - '
' + + " " + '
' + - '
' + t('settings.blur.sidebar') + '12 px
' + + '
' + + t("settings.blur.sidebar") + + '12 px
' + ' ' + - '
' + - '' + - '
' + - '
' + t('settings.section.opacity') + '
' + + "
" + + "" + + "
" + + '
' + + t("settings.section.opacity") + + "
" + '
' + - '
' + t('settings.opacity.header') + '45 %
' + + '
' + + t("settings.opacity.header") + + '45 %
' + ' ' + - '
' + + "
" + '
' + - '
' + t('settings.opacity.sidebar') + '45 %
' + + '
' + + t("settings.opacity.sidebar") + + '45 %
' + ' ' + - '
' + + " " + '
' + - '
' + t('settings.opacity.toolblock') + '55 %
' + + '
' + + t("settings.opacity.toolblock") + + '55 %
' + ' ' + - '
' + + " " + '
' + - '
' + t('settings.blur.toolblock') + '0 px
' + + '
' + + t("settings.blur.toolblock") + + '0 px
' + ' ' + - '
' + - '' + - '
' + - '
' + t('settings.section.overlay') + '
' + + "
" + + "" + + "
" + + '
' + + t("settings.section.overlay") + + "
" + '
' + - '
' + t('settings.overlay.opacity') + '72 %
' + + '
' + + t("settings.overlay.opacity") + + '72 %
' + ' ' + - '
' + + "
" + '
' + - '
' + t('settings.overlay.blur') + '12 px
' + + '
' + + t("settings.overlay.blur") + + '12 px
' + ' ' + - '
' + + " " + '
' + - '
' + t('settings.overlay.width') + '300 px
' + + '
' + + t("settings.overlay.width") + + '300 px
' + ' ' + - '
' + + " " + '
' + - ' ' + t('settings.overlay.bgColor') + '' + + ' ' + + t("settings.overlay.bgColor") + + "" + '
' + ' ' + ' #111322' + - '
' + - '
' + + " " + + " " + '
' + - ' ' + t('settings.overlay.btnColor') + '' + + ' ' + + t("settings.overlay.btnColor") + + "" + '
' + ' ' + ' #8b93ff' + - '
' + - '
' + + " " + + " " + '
' + - '
' + t('settings.overlay.btnRadius') + '10 px
' + + '
' + + t("settings.overlay.btnRadius") + + '10 px
' + ' ' + - '
' + - '' + - '
' + - '
' + t('settings.section.effects') + '
' + + "
" + + "" + + "
" + + '
' + + t("settings.section.effects") + + "
" + '
' + ' ' + - '
' + - '
' + - '
' + - '
' + t('settings.section.inputGlass') + '
' + + " " + + "
" + + "" + + "
" + + '
' + + t("settings.section.inputGlass") + + "
" + '
' + ' ' + - '
' + + " " + + "
" + '
' + - '
' + t('settings.inputGlass.blur') + '12 px
' + + '
' + + t("settings.inputGlass.blur") + + '12 px
' + ' ' + - '
' + + " " + '
' + - '
' + t('settings.inputGlass.opacity') + '55 %
' + + '
' + + t("settings.inputGlass.opacity") + + '55 %
' + ' ' + - '
' + - '' + - '
' + - '
' + t('settings.section.dangerous') + '
' + + "
" + + "" + + "
" + + '
' + + t("settings.section.dangerous") + + "
" + ' ' + '
' + - ' ' + t('settings.dangerous.hint') + '' + - ' ' + - '
' + - '
' + - '
' + - '
' + t('settings.section.agent') + '
' + + ' ' + + t("settings.dangerous.hint") + + "" + + ' " + + "
" + + "" + + "
" + + '
' + + t("settings.section.agent") + + "
" + '
' + - '
' + t('settings.approval.hint') + '
' + + '
' + + t("settings.approval.hint") + + "
" + '
' + - ' ' + - ' ' + - ' ' + - '
' + - '
' + + ' " + + ' " + + ' " + + "
" + + " " + '
' + ' ' + + " " + ' ' + + " " + ' ' + + " " + ' ' + + " " + ' ' + - '
' + - '' + - '
' + - '
' + t('settings.section.background') + '
' + + " " + + "
" + + "" + + "
" + + '
' + + t("settings.section.background") + + "
" + '
' + - ' ' + - ' ' + - '
' + - '
' + items + '
' + - '
' + t('settings.bg.hint') + '
' + - '
' + - '
' + - '
' + t('tg.title') + '
' + + ' " + + ' " + + "
" + + '
' + + items + + "
" + + '
' + + t("settings.bg.hint") + + "
" + + "" + + "
" + + '
' + + t("tg.title") + + "
" + '
' + - '
' + t('tg.label.token') + '
' + + '
' + + t("tg.label.token") + + "
" + ' ' + - '
' + + "
" + '
' + - '
' + t('tg.label.chatId') + '
' + + '
' + + t("tg.label.chatId") + + "
" + ' ' + - '
' + + " " + + '
' + + '
' + + t("tg.label.allowedUserId") + + "
" + + ' ' + + "
" + + '
' + + '
' + + t("tg.label.notifyIgnore") + + "
" + + ' ' + + "
" + '
' + - ' ' + - ' ' + - ' ' + - '
' + + ' ' + + ' ' + + ' ' + + " " + '
' + - ' ' + - ' ' + - ' ' + - '
' + - '' + - '
' + - '
' + t('settings.section.service') + '
' + + ' " + + ' " + + ' " + + "
" + + "" + + "
" + + '
' + + t("settings.section.service") + + "
" + '
' + - ' ' + - ' ' + - ' ' + - '
' + - '
' + + ' " + + ' " + + ' " + + " " + + "" + // ===== Диагностика интеграции ===== - '
' + - '
' + t('settings.section.diagnostics') + '
' + + "
" + + '
' + + t("settings.section.diagnostics") + + "
" + '
' + - '
' + t('settings.diagnostics.title') + '
' + - '
' + t('settings.diagnostics.hint') + '
' + + '
' + + t("settings.diagnostics.title") + + "
" + + '
' + + t("settings.diagnostics.hint") + + "
" + '
' + - ' ' + - '
' + - '
' + - '
' + + ' " + + "
" + + " " + + "" + // ===== Модалка отчёта ===== '' + - ''; + ' " + + " " + + " " + + "" + ); } function escapeHtml(s) { return String(s) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); } /** * Обработчики клика по превью: сохраняем фон и применяем. */ function bindBackgroundGrid() { - const grid = document.querySelectorAll('#' + TAB_CONTENT_ID + ' .cuckoo-bg-item'); - grid.forEach(el => { - el.addEventListener('click', async () => { - const id = el.getAttribute('data-bg-id'); + const grid = document.querySelectorAll( + "#" + TAB_CONTENT_ID + " .cuckoo-bg-item", + ); + grid.forEach((el) => { + el.addEventListener("click", async () => { + const id = el.getAttribute("data-bg-id"); if (!id) return; // Мгновенно применяем background.apply(id); // Подсветка - document.querySelectorAll('#' + TAB_CONTENT_ID + ' .cuckoo-bg-item').forEach(x => x.classList.remove('cuckoo-bg-selected')); - el.classList.add('cuckoo-bg-selected'); + document + .querySelectorAll("#" + TAB_CONTENT_ID + " .cuckoo-bg-item") + .forEach((x) => x.classList.remove("cuckoo-bg-selected")); + el.classList.add("cuckoo-bg-selected"); // Сохраняем в settings.json try { - const res = await window.electronAPI.setCuckooSetting('background', id); + const res = await window.electronAPI.setCuckooSetting("background", id); if (!res || !res.success) { - console.error('[Cookie Code] Не удалось сохранить фон:', res && res.error); + console.error( + "[Cookie Code] Не удалось сохранить фон:", + res && res.error, + ); } } catch (err) { - console.error('[Cookie Code] Ошибка сохранения фона:', err.message); + console.error("[Cookie Code] Ошибка сохранения фона:", err.message); } }); }); @@ -465,12 +638,22 @@ function bindBackgroundGrid() { */ function backgroundItemHTML(b) { const uri = background.getPreviewUri(b.file); - const styleAttr = uri ? ' style="background-image: url(\'' + uri + '\');"' : ''; + const styleAttr = uri + ? " style=\"background-image: url('" + uri + "');\"" + : ""; return ( - '
' + - '
' + - '
' + escapeHtml(b.label) + '
' + - '
' + '
' + + '
" + + '
' + + escapeHtml(b.label) + + "
" + + "
" ); } @@ -479,11 +662,16 @@ function backgroundItemHTML(b) { * Повторно навешивает обработчики клика и подсвечивает текущий фон. */ async function refreshBackgroundGrid() { - const gridEl = document.querySelector('#' + TAB_CONTENT_ID + ' #cuckoo-bg-grid'); + const gridEl = document.querySelector( + "#" + TAB_CONTENT_ID + " #cuckoo-bg-grid", + ); if (!gridEl) return; // Перечитываем пользовательские фоны с диска await background.loadCustomBackgrounds(); - gridEl.innerHTML = background.getAllBackgrounds().map(backgroundItemHTML).join(''); + gridEl.innerHTML = background + .getAllBackgrounds() + .map(backgroundItemHTML) + .join(""); bindBackgroundGrid(); await refreshBackgroundSelection(); } @@ -492,19 +680,26 @@ async function refreshBackgroundGrid() { * Кнопки «Открыть папку фонов» и «Обновить». */ function bindBackgroundFolderButtons() { - const openBtn = document.querySelector('#' + TAB_CONTENT_ID + ' #cuckoo-bg-open-folder'); + const openBtn = document.querySelector( + "#" + TAB_CONTENT_ID + " #cuckoo-bg-open-folder", + ); if (openBtn) { - openBtn.addEventListener('click', async () => { + openBtn.addEventListener("click", async () => { try { await window.electronAPI.openCustomBackgroundsFolder(); } catch (err) { - console.error('[Cookie Code] Не удалось открыть папку фонов:', err.message); + console.error( + "[Cookie Code] Не удалось открыть папку фонов:", + err.message, + ); } }); } - const refreshBtn = document.querySelector('#' + TAB_CONTENT_ID + ' #cuckoo-bg-refresh'); + const refreshBtn = document.querySelector( + "#" + TAB_CONTENT_ID + " #cuckoo-bg-refresh", + ); if (refreshBtn) { - refreshBtn.addEventListener('click', () => { + refreshBtn.addEventListener("click", () => { refreshBackgroundGrid(); }); } @@ -517,112 +712,231 @@ function bindBackgroundFolderButtons() { */ function collectCurrentBlurSettings() { return { - backgroundBlur: Number((document.getElementById('cuckoo-blur-bg') || {}).value) || 0, - headerBlur: Number((document.getElementById('cuckoo-blur-header') || {}).value), - sidebarBlur: Number((document.getElementById('cuckoo-blur-sidebar') || {}).value), - headerOpacity: Number((document.getElementById('cuckoo-op-header') || {}).value), - sidebarOpacity: Number((document.getElementById('cuckoo-op-sidebar') || {}).value), - toolBlockOpacity: Number((document.getElementById('cuckoo-op-toolblock') || {}).value), - toolBlockBlur: Number((document.getElementById('cuckoo-blur-toolblock') || {}).value), - overlayOpacity: Number((document.getElementById('cuckoo-op-overlay') || {}).value), - overlayBlur: Number((document.getElementById('cuckoo-blur-overlay') || {}).value), - overlayWidth: Number((document.getElementById('cuckoo-width-overlay') || {}).value), - overlayBgColor: (document.getElementById('cuckoo-color-overlay-bg') || {}).value || '#111322', - overlayPrimaryColor: (document.getElementById('cuckoo-color-overlay-btn') || {}).value || '#8b93ff', - overlayBtnRadius: Number((document.getElementById('cuckoo-radius-overlay-btn') || {}).value), - inputGlassBlur: Number((document.getElementById('cuckoo-blur-input') || {}).value), - inputGlassOpacity: Number((document.getElementById('cuckoo-op-input') || {}).value), + backgroundBlur: + Number((document.getElementById("cuckoo-blur-bg") || {}).value) || 0, + headerBlur: Number( + (document.getElementById("cuckoo-blur-header") || {}).value, + ), + sidebarBlur: Number( + (document.getElementById("cuckoo-blur-sidebar") || {}).value, + ), + headerOpacity: Number( + (document.getElementById("cuckoo-op-header") || {}).value, + ), + sidebarOpacity: Number( + (document.getElementById("cuckoo-op-sidebar") || {}).value, + ), + toolBlockOpacity: Number( + (document.getElementById("cuckoo-op-toolblock") || {}).value, + ), + toolBlockBlur: Number( + (document.getElementById("cuckoo-blur-toolblock") || {}).value, + ), + overlayOpacity: Number( + (document.getElementById("cuckoo-op-overlay") || {}).value, + ), + overlayBlur: Number( + (document.getElementById("cuckoo-blur-overlay") || {}).value, + ), + overlayWidth: Number( + (document.getElementById("cuckoo-width-overlay") || {}).value, + ), + overlayBgColor: + (document.getElementById("cuckoo-color-overlay-bg") || {}).value || + "#111322", + overlayPrimaryColor: + (document.getElementById("cuckoo-color-overlay-btn") || {}).value || + "#8b93ff", + overlayBtnRadius: Number( + (document.getElementById("cuckoo-radius-overlay-btn") || {}).value, + ), + inputGlassBlur: Number( + (document.getElementById("cuckoo-blur-input") || {}).value, + ), + inputGlassOpacity: Number( + (document.getElementById("cuckoo-op-input") || {}).value, + ), }; } function bindBlurSliders() { const sliders = [ - { inputId: 'cuckoo-blur-bg', valId: 'cuckoo-blur-bg-val', key: 'backgroundBlur', unit: ' px' }, - { inputId: 'cuckoo-blur-header', valId: 'cuckoo-blur-header-val', key: 'headerBlur', unit: ' px' }, - { inputId: 'cuckoo-blur-sidebar', valId: 'cuckoo-blur-sidebar-val', key: 'sidebarBlur', unit: ' px' }, - { inputId: 'cuckoo-op-header', valId: 'cuckoo-op-header-val', key: 'headerOpacity', unit: ' %' }, - { inputId: 'cuckoo-op-sidebar', valId: 'cuckoo-op-sidebar-val', key: 'sidebarOpacity', unit: ' %' }, - { inputId: 'cuckoo-op-toolblock', valId: 'cuckoo-op-toolblock-val', key: 'toolBlockOpacity', unit: ' %' }, - { inputId: 'cuckoo-blur-toolblock', valId: 'cuckoo-blur-toolblock-val', key: 'toolBlockBlur', unit: ' px' }, + { + inputId: "cuckoo-blur-bg", + valId: "cuckoo-blur-bg-val", + key: "backgroundBlur", + unit: " px", + }, + { + inputId: "cuckoo-blur-header", + valId: "cuckoo-blur-header-val", + key: "headerBlur", + unit: " px", + }, + { + inputId: "cuckoo-blur-sidebar", + valId: "cuckoo-blur-sidebar-val", + key: "sidebarBlur", + unit: " px", + }, + { + inputId: "cuckoo-op-header", + valId: "cuckoo-op-header-val", + key: "headerOpacity", + unit: " %", + }, + { + inputId: "cuckoo-op-sidebar", + valId: "cuckoo-op-sidebar-val", + key: "sidebarOpacity", + unit: " %", + }, + { + inputId: "cuckoo-op-toolblock", + valId: "cuckoo-op-toolblock-val", + key: "toolBlockOpacity", + unit: " %", + }, + { + inputId: "cuckoo-blur-toolblock", + valId: "cuckoo-blur-toolblock-val", + key: "toolBlockBlur", + unit: " px", + }, // Стеклянное поле ввода - { inputId: 'cuckoo-blur-input', valId: 'cuckoo-blur-input-val', key: 'inputGlassBlur', unit: ' px' }, - { inputId: 'cuckoo-op-input', valId: 'cuckoo-op-input-val', key: 'inputGlassOpacity', unit: ' %' }, + { + inputId: "cuckoo-blur-input", + valId: "cuckoo-blur-input-val", + key: "inputGlassBlur", + unit: " px", + }, + { + inputId: "cuckoo-op-input", + valId: "cuckoo-op-input-val", + key: "inputGlassOpacity", + unit: " %", + }, // Панель Cookie Code - { inputId: 'cuckoo-op-overlay', valId: 'cuckoo-op-overlay-val', key: 'overlayOpacity', unit: ' %' }, - { inputId: 'cuckoo-blur-overlay', valId: 'cuckoo-blur-overlay-val', key: 'overlayBlur', unit: ' px' }, - { inputId: 'cuckoo-width-overlay', valId: 'cuckoo-width-overlay-val', key: 'overlayWidth', unit: ' px' }, - { inputId: 'cuckoo-radius-overlay-btn', valId: 'cuckoo-radius-overlay-btn-val', key: 'overlayBtnRadius', unit: ' px' }, + { + inputId: "cuckoo-op-overlay", + valId: "cuckoo-op-overlay-val", + key: "overlayOpacity", + unit: " %", + }, + { + inputId: "cuckoo-blur-overlay", + valId: "cuckoo-blur-overlay-val", + key: "overlayBlur", + unit: " px", + }, + { + inputId: "cuckoo-width-overlay", + valId: "cuckoo-width-overlay-val", + key: "overlayWidth", + unit: " px", + }, + { + inputId: "cuckoo-radius-overlay-btn", + valId: "cuckoo-radius-overlay-btn-val", + key: "overlayBtnRadius", + unit: " px", + }, ]; sliders.forEach(({ inputId, valId, key, unit }) => { const input = document.getElementById(inputId); const label = document.getElementById(valId); if (!input || !label) return; - const updateLabel = (v) => { label.textContent = v + unit; }; + const updateLabel = (v) => { + label.textContent = v + unit; + }; - input.addEventListener('input', () => { + input.addEventListener("input", () => { const v = Number(input.value); updateLabel(v); background.applyBlur(collectCurrentBlurSettings()); }); - input.addEventListener('change', async () => { + input.addEventListener("change", async () => { const v = Number(input.value); try { const res = await window.electronAPI.setCuckooSetting(key, v); if (!res || !res.success) { - console.error('[Cookie Code] Не удалось сохранить настройку', key, res && res.error); + console.error( + "[Cookie Code] Не удалось сохранить настройку", + key, + res && res.error, + ); } } catch (err) { - console.error('[Cookie Code] Ошибка сохранения настройки', key, err.message); + console.error( + "[Cookie Code] Ошибка сохранения настройки", + key, + err.message, + ); } }); }); // Чекбокс «Стекло поля ввода включено» - const inputGlassCb = document.getElementById('cuckoo-input-glass-enabled'); + const inputGlassCb = document.getElementById("cuckoo-input-glass-enabled"); if (inputGlassCb) { - inputGlassCb.addEventListener('change', async () => { + inputGlassCb.addEventListener("change", async () => { const enabled = inputGlassCb.checked; background.applyInputGlassEnabled(enabled); try { - await window.electronAPI.setCuckooSetting('inputGlassEnabled', enabled); + await window.electronAPI.setCuckooSetting("inputGlassEnabled", enabled); } catch (err) { - console.error('[Cookie Code] Не удалось сохранить inputGlassEnabled:', err.message); + console.error( + "[Cookie Code] Не удалось сохранить inputGlassEnabled:", + err.message, + ); } }); } // Цвет фона панели - const bgInput = document.getElementById('cuckoo-color-overlay-bg'); - const bgLabel = document.getElementById('cuckoo-color-overlay-bg-val'); + const bgInput = document.getElementById("cuckoo-color-overlay-bg"); + const bgLabel = document.getElementById("cuckoo-color-overlay-bg-val"); if (bgInput) { - bgInput.addEventListener('input', () => { + bgInput.addEventListener("input", () => { if (bgLabel) bgLabel.textContent = bgInput.value; background.applyBlur(collectCurrentBlurSettings()); }); - bgInput.addEventListener('change', async () => { + bgInput.addEventListener("change", async () => { try { - await window.electronAPI.setCuckooSetting('overlayBgColor', bgInput.value); + await window.electronAPI.setCuckooSetting( + "overlayBgColor", + bgInput.value, + ); } catch (err) { - console.error('[Cookie Code] Ошибка сохранения overlayBgColor:', err.message); + console.error( + "[Cookie Code] Ошибка сохранения overlayBgColor:", + err.message, + ); } }); } // Цвет кнопок оверлея - const btnInput = document.getElementById('cuckoo-color-overlay-btn'); - const btnLabel = document.getElementById('cuckoo-color-overlay-btn-val'); + const btnInput = document.getElementById("cuckoo-color-overlay-btn"); + const btnLabel = document.getElementById("cuckoo-color-overlay-btn-val"); if (btnInput) { - btnInput.addEventListener('input', () => { + btnInput.addEventListener("input", () => { if (btnLabel) btnLabel.textContent = btnInput.value; background.applyBlur(collectCurrentBlurSettings()); }); - btnInput.addEventListener('change', async () => { + btnInput.addEventListener("change", async () => { try { - await window.electronAPI.setCuckooSetting('overlayPrimaryColor', btnInput.value); + await window.electronAPI.setCuckooSetting( + "overlayPrimaryColor", + btnInput.value, + ); } catch (err) { - console.error('[Cookie Code] Ошибка сохранения overlayPrimaryColor:', err.message); + console.error( + "[Cookie Code] Ошибка сохранения overlayPrimaryColor:", + err.message, + ); } }); } @@ -637,18 +951,28 @@ function bindBlurSliders() { async function refreshTelegramSettings() { try { const s = await window.electronAPI.getCuckooSettings(); - const tokenEl = document.getElementById('cuckoo-tg-token'); - const chatEl = document.getElementById('cuckoo-tg-chatid'); - const enabledEl = document.getElementById('cuckoo-tg-enabled'); - const notifyEl = document.getElementById('cuckoo-tg-notify'); - const feedEl = document.getElementById('cuckoo-tg-feed'); - if (tokenEl) tokenEl.value = s.telegramBotToken || ''; - if (chatEl) chatEl.value = s.telegramChatId || ''; + const tokenEl = document.getElementById("cuckoo-tg-token"); + const chatEl = document.getElementById("cuckoo-tg-chatid"); + const enabledEl = document.getElementById("cuckoo-tg-enabled"); + const notifyEl = document.getElementById("cuckoo-tg-notify"); + const feedEl = document.getElementById("cuckoo-tg-feed"); + const allowedUserEl = document.getElementById("cuckoo-tg-alloweduser"); + const notifyIgnoreEl = document.getElementById("cuckoo-tg-notify-ignore"); + if (tokenEl) tokenEl.value = s.telegramBotToken || ""; + if (chatEl) chatEl.value = s.telegramChatId || ""; + if (allowedUserEl) allowedUserEl.value = s.telegramAllowedUserId || ""; + if (notifyIgnoreEl) + notifyIgnoreEl.value = Array.isArray(s.telegramToolNotifyIgnore) + ? s.telegramToolNotifyIgnore.join("\n") + : ""; if (enabledEl) enabledEl.checked = !!s.telegramEnabled; if (notifyEl) notifyEl.checked = !!s.telegramNotifyTools; if (feedEl) feedEl.checked = !!s.telegramChatFeed; } catch (err) { - console.error('[Cookie Code] Не удалось загрузить настройки Telegram:', err.message); + console.error( + "[Cookie Code] Не удалось загрузить настройки Telegram:", + err.message, + ); } } @@ -656,64 +980,105 @@ async function refreshTelegramSettings() { * Обработчики кнопок Telegram: сохранить / проверить токен / тест. */ function bindTelegramSettings() { - const saveBtn = document.getElementById('cuckoo-tg-save'); - const pingBtn = document.getElementById('cuckoo-tg-ping'); - const testBtn = document.getElementById('cuckoo-tg-test'); + const saveBtn = document.getElementById("cuckoo-tg-save"); + const pingBtn = document.getElementById("cuckoo-tg-ping"); + const testBtn = document.getElementById("cuckoo-tg-test"); const readValues = () => ({ - token: (document.getElementById('cuckoo-tg-token') || {}).value || '', - chatId: (document.getElementById('cuckoo-tg-chatid') || {}).value || '', - enabled: !!(document.getElementById('cuckoo-tg-enabled') || {}).checked, - notify: !!(document.getElementById('cuckoo-tg-notify') || {}).checked, - feed: !!(document.getElementById('cuckoo-tg-feed') || {}).checked, + token: (document.getElementById("cuckoo-tg-token") || {}).value || "", + chatId: (document.getElementById("cuckoo-tg-chatid") || {}).value || "", + allowedUserId: + (document.getElementById("cuckoo-tg-alloweduser") || {}).value || "", + notifyIgnore: + (document.getElementById("cuckoo-tg-notify-ignore") || {}).value || "", + enabled: !!(document.getElementById("cuckoo-tg-enabled") || {}).checked, + notify: !!(document.getElementById("cuckoo-tg-notify") || {}).checked, + feed: !!(document.getElementById("cuckoo-tg-feed") || {}).checked, }); const save = async () => { const v = readValues(); try { - await window.electronAPI.setCuckooSetting('telegramBotToken', v.token.trim()); - await window.electronAPI.setCuckooSetting('telegramChatId', v.chatId.trim()); - await window.electronAPI.setCuckooSetting('telegramEnabled', v.enabled); - await window.electronAPI.setCuckooSetting('telegramNotifyTools', v.notify); - await window.electronAPI.setCuckooSetting('telegramChatFeed', v.feed); + await window.electronAPI.setCuckooSetting( + "telegramBotToken", + v.token.trim(), + ); + await window.electronAPI.setCuckooSetting( + "telegramChatId", + v.chatId.trim(), + ); + await window.electronAPI.setCuckooSetting( + "telegramAllowedUserId", + v.allowedUserId.trim(), + ); + await window.electronAPI.setCuckooSetting( + "telegramToolNotifyIgnore", + v.notifyIgnore + .split(/\r?\n/) + .map((x) => x.trim()) + .filter(Boolean), + ); + await window.electronAPI.setCuckooSetting("telegramEnabled", v.enabled); + await window.electronAPI.setCuckooSetting( + "telegramNotifyTools", + v.notify, + ); + await window.electronAPI.setCuckooSetting("telegramChatFeed", v.feed); const res = await window.electronAPI.telegramApply(); return res && res.success; } catch (err) { - console.error('[Cookie Code] Не удалось сохранить Telegram:', err.message); + console.error( + "[Cookie Code] Не удалось сохранить Telegram:", + err.message, + ); return false; } }; - saveBtn?.addEventListener('click', async () => { - saveBtn.textContent = '...'; + saveBtn?.addEventListener("click", async () => { + saveBtn.textContent = "..."; const ok = await save(); - saveBtn.textContent = ok ? '✅ ' + t('tg.btn.save') : '❌ ' + t('tg.btn.save'); - setTimeout(() => { saveBtn.textContent = t('tg.btn.save'); }, 1800); + saveBtn.textContent = ok + ? "✅ " + t("tg.btn.save") + : "❌ " + t("tg.btn.save"); + setTimeout(() => { + saveBtn.textContent = t("tg.btn.save"); + }, 1800); }); - pingBtn?.addEventListener('click', async () => { + pingBtn?.addEventListener("click", async () => { await save(); - pingBtn.textContent = '...'; + pingBtn.textContent = "..."; const res = await window.electronAPI.telegramPing(); - pingBtn.textContent = (res && res.success) ? '✅ @' + (res.username || 'bot') : '❌ ' + ((res && res.error) || 'error'); - setTimeout(() => { pingBtn.textContent = t('tg.btn.ping'); }, 2500); + pingBtn.textContent = + res && res.success + ? "✅ @" + (res.username || "bot") + : "❌ " + ((res && res.error) || "error"); + setTimeout(() => { + pingBtn.textContent = t("tg.btn.ping"); + }, 2500); }); - testBtn?.addEventListener('click', async () => { + testBtn?.addEventListener("click", async () => { await save(); - testBtn.textContent = '...'; + testBtn.textContent = "..."; const res = await window.electronAPI.telegramTest(); - testBtn.textContent = (res && res.success) ? '✅ ' + t('tg.btn.test') : '❌ ' + ((res && res.error) || 'error'); - setTimeout(() => { testBtn.textContent = t('tg.btn.test'); }, 2500); + testBtn.textContent = + res && res.success + ? "✅ " + t("tg.btn.test") + : "❌ " + ((res && res.error) || "error"); + setTimeout(() => { + testBtn.textContent = t("tg.btn.test"); + }, 2500); }); } function bindResetButton() { - const btn = document.getElementById('cuckoo-btn-reset'); + const btn = document.getElementById("cuckoo-btn-reset"); if (!btn) return; - btn.addEventListener('click', async () => { + btn.addEventListener("click", async () => { btn.disabled = true; - btn.textContent = t('settings.btn.resetting'); + btn.textContent = t("settings.btn.resetting"); try { await background.resetAll(); // Обновляем UI: слайдеры + подсветка фона @@ -721,17 +1086,17 @@ function bindResetButton() { await refreshBackgroundSelection(); } finally { btn.disabled = false; - btn.textContent = t('settings.btn.reset'); + btn.textContent = t("settings.btn.reset"); } }); // Кнопка «Очистить мета-данные» — чистит localStorage - const clearBtn = document.getElementById('cuckoo-btn-clear-storage'); + const clearBtn = document.getElementById("cuckoo-btn-clear-storage"); if (clearBtn) { - clearBtn.addEventListener('click', () => { + clearBtn.addEventListener("click", () => { clearBtn.disabled = true; const originalText = clearBtn.textContent; - clearBtn.textContent = t('settings.btn.clearing'); + clearBtn.textContent = t("settings.btn.clearing"); try { background.clearLocalStorage(); } finally { @@ -744,45 +1109,50 @@ function bindResetButton() { } // Кнопка «Диагностика интеграции» — прогоняет проверки провайдера и показывает отчёт. - const diagBtn = document.getElementById('cuckoo-btn-diagnostics'); + const diagBtn = document.getElementById("cuckoo-btn-diagnostics"); if (diagBtn) { - diagBtn.addEventListener('click', async () => { + diagBtn.addEventListener("click", async () => { diagBtn.disabled = true; const originalText = diagBtn.textContent; - diagBtn.textContent = t('settings.diagnostics.running'); + diagBtn.textContent = t("settings.diagnostics.running"); try { - const { runDiagnostics, formatReportText } = require('./diagnostics'); + const { runDiagnostics, formatReportText } = require("./diagnostics"); const report = await runDiagnostics(); const text = formatReportText(report); - const modal = document.getElementById('cuckoo-diag-modal'); - const body = document.getElementById('cuckoo-diag-body'); + const modal = document.getElementById("cuckoo-diag-modal"); + const body = document.getElementById("cuckoo-diag-body"); if (modal && body) { body.textContent = text; - modal.style.display = 'flex'; + modal.style.display = "flex"; // Копирование - const copyBtn = document.getElementById('cuckoo-diag-copy'); - const closeBtn = document.getElementById('cuckoo-diag-close'); - const close = () => { modal.style.display = 'none'; }; + const copyBtn = document.getElementById("cuckoo-diag-copy"); + const closeBtn = document.getElementById("cuckoo-diag-close"); + const close = () => { + modal.style.display = "none"; + }; if (closeBtn) closeBtn.onclick = close; - if (copyBtn) copyBtn.onclick = async () => { - try { - await navigator.clipboard.writeText(text); - copyBtn.textContent = t('settings.diagnostics.copied'); - setTimeout(() => { copyBtn.textContent = t('settings.diagnostics.copy'); }, 1500); - } catch (_) { - // Fallback — выделяем текст, чтобы пользователь скопировал вручную - if (body) { - const range = document.createRange(); - range.selectNodeContents(body); - const sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); + if (copyBtn) + copyBtn.onclick = async () => { + try { + await navigator.clipboard.writeText(text); + copyBtn.textContent = t("settings.diagnostics.copied"); + setTimeout(() => { + copyBtn.textContent = t("settings.diagnostics.copy"); + }, 1500); + } catch (_) { + // Fallback — выделяем текст, чтобы пользователь скопировал вручную + if (body) { + const range = document.createRange(); + range.selectNodeContents(body); + const sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } } - } - }; + }; } } catch (err) { - console.error('[Cookie Code] diagnostics error:', err && err.message); + console.error("[Cookie Code] diagnostics error:", err && err.message); } finally { diagBtn.disabled = false; diagBtn.textContent = originalText; @@ -791,17 +1161,23 @@ function bindResetButton() { } // Кнопка «Открыть файл настроек» — открывает cuckoo-settings.json системным редактором. - const openCfgBtn = document.getElementById('cuckoo-btn-open-config'); + const openCfgBtn = document.getElementById("cuckoo-btn-open-config"); if (openCfgBtn) { - openCfgBtn.addEventListener('click', async () => { + openCfgBtn.addEventListener("click", async () => { openCfgBtn.disabled = true; const originalText = openCfgBtn.textContent; try { const res = await window.electronAPI.openCuckooSettingsFile(); - openCfgBtn.textContent = (res && res.success) ? t('settings.btn.openConfig.opened') : t('settings.btn.openConfig.error'); + openCfgBtn.textContent = + res && res.success + ? t("settings.btn.openConfig.opened") + : t("settings.btn.openConfig.error"); } catch (err) { - console.error('[Cookie Code] openCuckooSettingsFile error:', err.message); - openCfgBtn.textContent = t('settings.btn.openConfig.error'); + console.error( + "[Cookie Code] openCuckooSettingsFile error:", + err.message, + ); + openCfgBtn.textContent = t("settings.btn.openConfig.error"); } finally { setTimeout(() => { openCfgBtn.disabled = false; @@ -816,35 +1192,50 @@ function bindResetButton() { * Чекбокс RGB-переливания ника. */ function bindRgbCheckbox() { - const cb = document.getElementById('cuckoo-rgb-username'); + const cb = document.getElementById("cuckoo-rgb-username"); if (!cb) return; - cb.addEventListener('change', async () => { + cb.addEventListener("change", async () => { const enabled = cb.checked; background.applyRgbUsername(enabled); try { - const res = await window.electronAPI.setCuckooSetting('rgbUsername', enabled); + const res = await window.electronAPI.setCuckooSetting( + "rgbUsername", + enabled, + ); if (!res || !res.success) { - console.error('[Cookie Code] Не удалось сохранить rgbUsername:', res && res.error); + console.error( + "[Cookie Code] Не удалось сохранить rgbUsername:", + res && res.error, + ); } } catch (err) { - console.error('[Cookie Code] Ошибка сохранения rgbUsername:', err.message); + console.error( + "[Cookie Code] Ошибка сохранения rgbUsername:", + err.message, + ); } }); } function bindCustomizationToggle() { - const btn = document.getElementById('cuckoo-customization-toggle'); + const btn = document.getElementById("cuckoo-customization-toggle"); if (!btn) return; - btn.addEventListener('click', async () => { + btn.addEventListener("click", async () => { btn.disabled = true; - btn.textContent = t('settings.customization.reloading'); + btn.textContent = t("settings.customization.reloading"); try { const settings = await window.electronAPI.getCuckooSettings(); const enabled = !settings || settings.customizationEnabled !== false; - await window.electronAPI.setCuckooSetting('customizationEnabled', !enabled); + await window.electronAPI.setCuckooSetting( + "customizationEnabled", + !enabled, + ); location.reload(); } catch (err) { - console.error('[Cookie Code] Ошибка переключения кастомизации:', err.message); + console.error( + "[Cookie Code] Ошибка переключения кастомизации:", + err.message, + ); btn.disabled = false; refreshCustomizationToggle(); } @@ -852,17 +1243,23 @@ function bindCustomizationToggle() { } async function refreshCustomizationToggle() { - const btn = document.getElementById('cuckoo-customization-toggle'); + const btn = document.getElementById("cuckoo-customization-toggle"); if (!btn) return; try { const settings = await window.electronAPI.getCuckooSettings(); const enabled = !settings || settings.customizationEnabled !== false; - btn.textContent = enabled ? t('settings.customization.disable') : t('settings.customization.enable'); - btn.style.borderColor = enabled ? 'rgba(255,107,122,0.5)' : 'rgba(93,214,157,0.55)'; - btn.style.background = enabled ? 'rgba(255,107,122,0.12)' : 'rgba(93,214,157,0.12)'; - btn.style.color = enabled ? '#ffb0b8' : '#a7f0c9'; + btn.textContent = enabled + ? t("settings.customization.disable") + : t("settings.customization.enable"); + btn.style.borderColor = enabled + ? "rgba(255,107,122,0.5)" + : "rgba(93,214,157,0.55)"; + btn.style.background = enabled + ? "rgba(255,107,122,0.12)" + : "rgba(93,214,157,0.12)"; + btn.style.color = enabled ? "#ffb0b8" : "#a7f0c9"; } catch (_) { - btn.textContent = t('settings.customization.disable'); + btn.textContent = t("settings.customization.disable"); } } @@ -872,31 +1269,31 @@ async function refreshCustomizationToggle() { * чтобы тексты в OVERLAY_HTML пересобрались с новым языком. */ function bindLanguageButtons() { - const buttons = document.querySelectorAll('.cuckoo-lang-btn'); + const buttons = document.querySelectorAll(".cuckoo-lang-btn"); if (!buttons.length) return; // Подсветить активный - const current = (window.__cuckooI18nLang || 'ru'); - buttons.forEach(btn => { - if (btn.getAttribute('data-lang') === current) { - btn.style.background = 'rgba(139,147,255,0.35)'; - btn.style.color = '#fff'; - btn.style.borderColor = 'rgba(139,147,255,0.8)'; + const current = window.__cuckooI18nLang || "ru"; + buttons.forEach((btn) => { + if (btn.getAttribute("data-lang") === current) { + btn.style.background = "rgba(139,147,255,0.35)"; + btn.style.color = "#fff"; + btn.style.borderColor = "rgba(139,147,255,0.8)"; } else { - btn.style.background = 'rgba(139,147,255,0.12)'; - btn.style.color = '#cfd3ff'; - btn.style.borderColor = 'rgba(139,147,255,0.4)'; + btn.style.background = "rgba(139,147,255,0.12)"; + btn.style.color = "#cfd3ff"; + btn.style.borderColor = "rgba(139,147,255,0.4)"; } }); - buttons.forEach(btn => { - btn.addEventListener('click', async () => { - const lang = btn.getAttribute('data-lang'); + buttons.forEach((btn) => { + btn.addEventListener("click", async () => { + const lang = btn.getAttribute("data-lang"); if (!lang || lang === current) return; try { - await window.electronAPI.setCuckooSetting('language', lang); + await window.electronAPI.setCuckooSetting("language", lang); } catch (err) { - console.error('[Cookie Code] Не удалось сохранить язык:', err.message); + console.error("[Cookie Code] Не удалось сохранить язык:", err.message); } // Перезагружаем страницу — preload пересоберёт HTML с новым языком location.reload(); @@ -908,38 +1305,58 @@ function bindLanguageButtons() { * Textarea со списком опасных regex-паттернов + кнопка «Сохранить». */ async function bindDangerousPatterns() { - const ta = document.getElementById('cuckoo-dangerous-patterns'); - const btn = document.getElementById('cuckoo-btn-save-dangerous'); + const ta = document.getElementById("cuckoo-dangerous-patterns"); + const btn = document.getElementById("cuckoo-btn-save-dangerous"); if (!ta || !btn) return; // Загружаем текущий список try { const s = await window.electronAPI.getCuckooSettings(); - const list = Array.isArray(s && s.dangerousPatterns) ? s.dangerousPatterns : []; - ta.value = list.join('\n'); + const list = Array.isArray(s && s.dangerousPatterns) + ? s.dangerousPatterns + : []; + ta.value = list.join("\n"); } catch (_) {} - btn.addEventListener('click', async () => { - const raw = (ta.value || ''); + btn.addEventListener("click", async () => { + const raw = ta.value || ""; // Разбиваем по строкам, убираем пустые и пробелы - const patterns = raw.split(/\r?\n/).map(s => s.trim()).filter(Boolean); + const patterns = raw + .split(/\r?\n/) + .map((s) => s.trim()) + .filter(Boolean); btn.disabled = true; const original = btn.textContent; - btn.textContent = t('settings.dangerous.saving'); + btn.textContent = t("settings.dangerous.saving"); try { - const res = await window.electronAPI.setCuckooSetting('dangerousPatterns', patterns); + const res = await window.electronAPI.setCuckooSetting( + "dangerousPatterns", + patterns, + ); if (res && res.success) { - btn.textContent = t('settings.dangerous.saved'); - setTimeout(() => { btn.textContent = original; btn.disabled = false; }, 1200); + btn.textContent = t("settings.dangerous.saved"); + setTimeout(() => { + btn.textContent = original; + btn.disabled = false; + }, 1200); } else { - btn.textContent = t('settings.dangerous.error'); - setTimeout(() => { btn.textContent = original; btn.disabled = false; }, 1500); + btn.textContent = t("settings.dangerous.error"); + setTimeout(() => { + btn.textContent = original; + btn.disabled = false; + }, 1500); } } catch (err) { - console.error('[Cookie Code] Не удалось сохранить опасные команды:', err.message); - btn.textContent = t('settings.dangerous.error'); - setTimeout(() => { btn.textContent = original; btn.disabled = false; }, 1500); + console.error( + "[Cookie Code] Не удалось сохранить опасные команды:", + err.message, + ); + btn.textContent = t("settings.dangerous.error"); + setTimeout(() => { + btn.textContent = original; + btn.disabled = false; + }, 1500); } }); } @@ -952,10 +1369,10 @@ async function bindDangerousPatterns() { * в cuckoo-settings.json — перезагрузка страницы не требуется. */ function bindAgentSettings() { - const buttons = document.querySelectorAll('.cuckoo-approval-btn'); - buttons.forEach(btn => { - btn.addEventListener('click', async () => { - const mode = btn.getAttribute('data-mode'); + const buttons = document.querySelectorAll(".cuckoo-approval-btn"); + buttons.forEach((btn) => { + btn.addEventListener("click", async () => { + const mode = btn.getAttribute("data-mode"); if (!mode) return; // Мгновенно применяем в рантайме state.toolApprovalMode = mode; @@ -963,89 +1380,116 @@ function bindAgentSettings() { applyApprovalActiveStyle(mode); // Сохраняем в settings.json try { - await window.electronAPI.setCuckooSetting('toolApprovalMode', mode); + await window.electronAPI.setCuckooSetting("toolApprovalMode", mode); } catch (err) { - console.error('[Cookie Code] Не удалось сохранить toolApprovalMode:', err.message); + console.error( + "[Cookie Code] Не удалось сохранить toolApprovalMode:", + err.message, + ); } }); }); - const cb = document.getElementById('cuckoo-hide-system-messages'); + const cb = document.getElementById("cuckoo-hide-system-messages"); if (cb) { - cb.addEventListener('change', async () => { + cb.addEventListener("change", async () => { const enabled = cb.checked; // Мгновенно применяем: stealth-модуль читает state при каждом проходе state.hideSystemMessages = enabled; try { - await window.electronAPI.setCuckooSetting('hideSystemMessages', enabled); + await window.electronAPI.setCuckooSetting( + "hideSystemMessages", + enabled, + ); } catch (err) { - console.error('[Cookie Code] Не удалось сохранить hideSystemMessages:', err.message); + console.error( + "[Cookie Code] Не удалось сохранить hideSystemMessages:", + err.message, + ); } }); } - const chipCb = document.getElementById('cuckoo-file-chip-enabled'); + const chipCb = document.getElementById("cuckoo-file-chip-enabled"); if (chipCb) { - chipCb.addEventListener('change', async () => { + chipCb.addEventListener("change", async () => { const enabled = chipCb.checked; // Мгновенно применяем в рантайме: модуль file-chip снимет/применит чипы. state.fileChipEnabled = enabled; try { - const fileChip = require('./file-chip'); + const fileChip = require("./file-chip"); fileChip.setEnabled(enabled); } catch (err) { - console.error('[Cookie Code] fileChip.setEnabled error:', err.message); + console.error("[Cookie Code] fileChip.setEnabled error:", err.message); } try { - await window.electronAPI.setCuckooSetting('fileChipEnabled', enabled); + await window.electronAPI.setCuckooSetting("fileChipEnabled", enabled); } catch (err) { - console.error('[Cookie Code] Не удалось сохранить fileChipEnabled:', err.message); + console.error( + "[Cookie Code] Не удалось сохранить fileChipEnabled:", + err.message, + ); } }); } - const producedCb = document.getElementById('cuckoo-show-produced-enabled'); + const producedCb = document.getElementById("cuckoo-show-produced-enabled"); if (producedCb) { - producedCb.addEventListener('change', async () => { + producedCb.addEventListener("change", async () => { const enabled = producedCb.checked; state.showProducedFiles = enabled; try { - const responseMeta = require('./response-meta'); - if (typeof responseMeta.setEnabled === 'function') responseMeta.setEnabled(enabled); + const responseMeta = require("./response-meta"); + if (typeof responseMeta.setEnabled === "function") + responseMeta.setEnabled(enabled); } catch (err) { - console.error('[Cookie Code] responseMeta.setEnabled error:', err.message); + console.error( + "[Cookie Code] responseMeta.setEnabled error:", + err.message, + ); } try { - await window.electronAPI.setCuckooSetting('showProducedFiles', enabled); + await window.electronAPI.setCuckooSetting("showProducedFiles", enabled); } catch (err) { - console.error('[Cookie Code] Не удалось сохранить showProducedFiles:', err.message); + console.error( + "[Cookie Code] Не удалось сохранить showProducedFiles:", + err.message, + ); } }); } - const fmtCb = document.getElementById('cuckoo-formatters-enabled'); + const fmtCb = document.getElementById("cuckoo-formatters-enabled"); if (fmtCb) { - fmtCb.addEventListener('change', async () => { + fmtCb.addEventListener("change", async () => { const enabled = fmtCb.checked; // Применяется при следующем write/edit (читается из settings.json на главном процессе). try { - await window.electronAPI.setCuckooSetting('formattersEnabled', enabled); + await window.electronAPI.setCuckooSetting("formattersEnabled", enabled); } catch (err) { - console.error('[Cookie Code] Не удалось сохранить formattersEnabled:', err.message); + console.error( + "[Cookie Code] Не удалось сохранить formattersEnabled:", + err.message, + ); } }); } - const convTokensCb = document.getElementById('cuckoo-show-conv-tokens'); + const convTokensCb = document.getElementById("cuckoo-show-conv-tokens"); if (convTokensCb) { - convTokensCb.addEventListener('change', async () => { + convTokensCb.addEventListener("change", async () => { const enabled = convTokensCb.checked; state.showConvTokens = enabled; - try { applyConvTokensVisibility(enabled); } catch (_) {} try { - await window.electronAPI.setCuckooSetting('showConvTokens', enabled); + applyConvTokensVisibility(enabled); + } catch (_) {} + try { + await window.electronAPI.setCuckooSetting("showConvTokens", enabled); } catch (err) { - console.error('[Cookie Code] Не удалось сохранить showConvTokens:', err.message); + console.error( + "[Cookie Code] Не удалось сохранить showConvTokens:", + err.message, + ); } }); } @@ -1056,13 +1500,13 @@ function bindAgentSettings() { * @param {boolean} on */ function applyConvTokensVisibility(on) { - const section = document.querySelector('.cuckoo-token-section'); + const section = document.querySelector(".cuckoo-token-section"); if (!section) return; - section.style.display = on ? '' : 'none'; + section.style.display = on ? "" : "none"; // Скрываем и соседний разделитель перед блоком, если он есть. const prev = section.previousElementSibling; - if (prev && prev.classList && prev.classList.contains('cuckoo-divider')) { - prev.style.display = on ? '' : 'none'; + if (prev && prev.classList && prev.classList.contains("cuckoo-divider")) { + prev.style.display = on ? "" : "none"; } } @@ -1070,16 +1514,16 @@ function applyConvTokensVisibility(on) { * Подсветить активный режим подтверждения (как кнопки языка RU/EN). */ function applyApprovalActiveStyle(mode) { - const buttons = document.querySelectorAll('.cuckoo-approval-btn'); - buttons.forEach(btn => { - if (btn.getAttribute('data-mode') === mode) { - btn.style.background = 'rgba(139,147,255,0.35)'; - btn.style.color = '#fff'; - btn.style.borderColor = 'rgba(139,147,255,0.8)'; + const buttons = document.querySelectorAll(".cuckoo-approval-btn"); + buttons.forEach((btn) => { + if (btn.getAttribute("data-mode") === mode) { + btn.style.background = "rgba(139,147,255,0.35)"; + btn.style.color = "#fff"; + btn.style.borderColor = "rgba(139,147,255,0.8)"; } else { - btn.style.background = 'rgba(139,147,255,0.12)'; - btn.style.color = '#cfd3ff'; - btn.style.borderColor = 'rgba(139,147,255,0.4)'; + btn.style.background = "rgba(139,147,255,0.12)"; + btn.style.color = "#cfd3ff"; + btn.style.borderColor = "rgba(139,147,255,0.4)"; } }); } @@ -1090,34 +1534,36 @@ function applyApprovalActiveStyle(mode) { async function refreshAgentSettings() { try { const s = await window.electronAPI.getCuckooSettings(); - const mode = (s && s.toolApprovalMode) || 'off'; + const mode = (s && s.toolApprovalMode) || "off"; state.toolApprovalMode = mode; applyApprovalActiveStyle(mode); - const cb = document.getElementById('cuckoo-hide-system-messages'); + const cb = document.getElementById("cuckoo-hide-system-messages"); if (cb) { cb.checked = Boolean(s && s.hideSystemMessages === true); state.hideSystemMessages = cb.checked; } - const chipCb = document.getElementById('cuckoo-file-chip-enabled'); + const chipCb = document.getElementById("cuckoo-file-chip-enabled"); if (chipCb) { chipCb.checked = !s || s.fileChipEnabled !== false; state.fileChipEnabled = chipCb.checked; } - const producedCb = document.getElementById('cuckoo-show-produced-enabled'); + const producedCb = document.getElementById("cuckoo-show-produced-enabled"); if (producedCb) { producedCb.checked = !s || s.showProducedFiles !== false; state.showProducedFiles = producedCb.checked; } - const fmtCb = document.getElementById('cuckoo-formatters-enabled'); + const fmtCb = document.getElementById("cuckoo-formatters-enabled"); if (fmtCb) { fmtCb.checked = !s || s.formattersEnabled !== false; } - const convTokensCb = document.getElementById('cuckoo-show-conv-tokens'); + const convTokensCb = document.getElementById("cuckoo-show-conv-tokens"); if (convTokensCb) { // По умолчанию выключено: показываем только если явно true. convTokensCb.checked = Boolean(s && s.showConvTokens === true); state.showConvTokens = convTokensCb.checked; - try { applyConvTokensVisibility(convTokensCb.checked); } catch (_) {} + try { + applyConvTokensVisibility(convTokensCb.checked); + } catch (_) {} } } catch (_) {} } @@ -1128,7 +1574,7 @@ async function refreshAgentSettings() { async function refreshRgbCheckbox() { try { const s = await window.electronAPI.getCuckooSettings(); - const cb = document.getElementById('cuckoo-rgb-username'); + const cb = document.getElementById("cuckoo-rgb-username"); if (cb) cb.checked = s && s.rgbUsername !== false; } catch (_) {} } @@ -1139,30 +1585,95 @@ async function refreshRgbCheckbox() { async function refreshBlurValues() { try { const s = await window.electronAPI.getCuckooSettings(); - const setSlider = (id, valId, v, unit = ' px') => { + const setSlider = (id, valId, v, unit = " px") => { const input = document.getElementById(id); const label = document.getElementById(valId); if (!input || !label) return; input.value = String(v); label.textContent = v + unit; }; - setSlider('cuckoo-blur-bg', 'cuckoo-blur-bg-val', Number(s && s.backgroundBlur) || 0, ' px'); - setSlider('cuckoo-blur-header', 'cuckoo-blur-header-val', Number(s && s.headerBlur != null ? s.headerBlur : 12), ' px'); - setSlider('cuckoo-blur-sidebar', 'cuckoo-blur-sidebar-val', Number(s && s.sidebarBlur != null ? s.sidebarBlur : 12), ' px'); - setSlider('cuckoo-op-header', 'cuckoo-op-header-val', Number(s && s.headerOpacity != null ? s.headerOpacity : 45), ' %'); - setSlider('cuckoo-op-sidebar', 'cuckoo-op-sidebar-val', Number(s && s.sidebarOpacity != null ? s.sidebarOpacity : 45), ' %'); - setSlider('cuckoo-op-toolblock', 'cuckoo-op-toolblock-val', Number(s && s.toolBlockOpacity != null ? s.toolBlockOpacity : 55), ' %'); - setSlider('cuckoo-blur-toolblock', 'cuckoo-blur-toolblock-val', Number(s && s.toolBlockBlur != null ? s.toolBlockBlur : 0), ' px'); - setSlider('cuckoo-blur-input', 'cuckoo-blur-input-val', Number(s && s.inputGlassBlur != null ? s.inputGlassBlur : 12), ' px'); - setSlider('cuckoo-op-input', 'cuckoo-op-input-val', Number(s && s.inputGlassOpacity != null ? s.inputGlassOpacity : 55), ' %'); - const igCb = document.getElementById('cuckoo-input-glass-enabled'); + setSlider( + "cuckoo-blur-bg", + "cuckoo-blur-bg-val", + Number(s && s.backgroundBlur) || 0, + " px", + ); + setSlider( + "cuckoo-blur-header", + "cuckoo-blur-header-val", + Number(s && s.headerBlur != null ? s.headerBlur : 12), + " px", + ); + setSlider( + "cuckoo-blur-sidebar", + "cuckoo-blur-sidebar-val", + Number(s && s.sidebarBlur != null ? s.sidebarBlur : 12), + " px", + ); + setSlider( + "cuckoo-op-header", + "cuckoo-op-header-val", + Number(s && s.headerOpacity != null ? s.headerOpacity : 45), + " %", + ); + setSlider( + "cuckoo-op-sidebar", + "cuckoo-op-sidebar-val", + Number(s && s.sidebarOpacity != null ? s.sidebarOpacity : 45), + " %", + ); + setSlider( + "cuckoo-op-toolblock", + "cuckoo-op-toolblock-val", + Number(s && s.toolBlockOpacity != null ? s.toolBlockOpacity : 55), + " %", + ); + setSlider( + "cuckoo-blur-toolblock", + "cuckoo-blur-toolblock-val", + Number(s && s.toolBlockBlur != null ? s.toolBlockBlur : 0), + " px", + ); + setSlider( + "cuckoo-blur-input", + "cuckoo-blur-input-val", + Number(s && s.inputGlassBlur != null ? s.inputGlassBlur : 12), + " px", + ); + setSlider( + "cuckoo-op-input", + "cuckoo-op-input-val", + Number(s && s.inputGlassOpacity != null ? s.inputGlassOpacity : 55), + " %", + ); + const igCb = document.getElementById("cuckoo-input-glass-enabled"); if (igCb) igCb.checked = Boolean(s && s.inputGlassEnabled === true); // Панель Cookie Code - setSlider('cuckoo-op-overlay', 'cuckoo-op-overlay-val', Number(s && s.overlayOpacity != null ? s.overlayOpacity : 72), ' %'); - setSlider('cuckoo-blur-overlay', 'cuckoo-blur-overlay-val', Number(s && s.overlayBlur != null ? s.overlayBlur : 12), ' px'); - setSlider('cuckoo-width-overlay', 'cuckoo-width-overlay-val', Number(s && s.overlayWidth != null ? s.overlayWidth : 300), ' px'); - setSlider('cuckoo-radius-overlay-btn', 'cuckoo-radius-overlay-btn-val', Number(s && s.overlayBtnRadius != null ? s.overlayBtnRadius : 10), ' px'); + setSlider( + "cuckoo-op-overlay", + "cuckoo-op-overlay-val", + Number(s && s.overlayOpacity != null ? s.overlayOpacity : 72), + " %", + ); + setSlider( + "cuckoo-blur-overlay", + "cuckoo-blur-overlay-val", + Number(s && s.overlayBlur != null ? s.overlayBlur : 12), + " px", + ); + setSlider( + "cuckoo-width-overlay", + "cuckoo-width-overlay-val", + Number(s && s.overlayWidth != null ? s.overlayWidth : 300), + " px", + ); + setSlider( + "cuckoo-radius-overlay-btn", + "cuckoo-radius-overlay-btn-val", + Number(s && s.overlayBtnRadius != null ? s.overlayBtnRadius : 10), + " px", + ); const setColor = (inputId, valId, v) => { const input = document.getElementById(inputId); @@ -1170,10 +1681,21 @@ async function refreshBlurValues() { if (input) input.value = v; if (label) label.textContent = v; }; - setColor('cuckoo-color-overlay-bg', 'cuckoo-color-overlay-bg-val', (s && s.overlayBgColor) || '#111322'); - setColor('cuckoo-color-overlay-btn', 'cuckoo-color-overlay-btn-val', (s && s.overlayPrimaryColor) || '#8b93ff'); + setColor( + "cuckoo-color-overlay-bg", + "cuckoo-color-overlay-bg-val", + (s && s.overlayBgColor) || "#111322", + ); + setColor( + "cuckoo-color-overlay-btn", + "cuckoo-color-overlay-btn-val", + (s && s.overlayPrimaryColor) || "#8b93ff", + ); } catch (err) { - console.error('[Cookie Code] Не удалось загрузить значения блюра:', err.message); + console.error( + "[Cookie Code] Не удалось загрузить значения блюра:", + err.message, + ); } } @@ -1184,15 +1706,20 @@ async function refreshBackgroundSelection() { try { const settings = await window.electronAPI.getCuckooSettings(); const current = (settings && settings.background) || background.DEFAULT_ID; - document.querySelectorAll('#' + TAB_CONTENT_ID + ' .cuckoo-bg-item').forEach(el => { - if (el.getAttribute('data-bg-id') === current) { - el.classList.add('cuckoo-bg-selected'); - } else { - el.classList.remove('cuckoo-bg-selected'); - } - }); + document + .querySelectorAll("#" + TAB_CONTENT_ID + " .cuckoo-bg-item") + .forEach((el) => { + if (el.getAttribute("data-bg-id") === current) { + el.classList.add("cuckoo-bg-selected"); + } else { + el.classList.remove("cuckoo-bg-selected"); + } + }); } catch (err) { - console.error('[Cookie Code] Не удалось прочитать текущий фон:', err.message); + console.error( + "[Cookie Code] Не удалось прочитать текущий фон:", + err.message, + ); } } @@ -1201,9 +1728,13 @@ async function refreshBackgroundSelection() { */ function deactivateCuckooTab() { const nativeScroll = document.querySelector( - MODAL_CONTENT_MAIN_SELECTOR + ' ' + RIGHT_PANEL_WRAPPER_SELECTOR + ' ' + NATIVE_SCROLL_AREA_SELECTOR + MODAL_CONTENT_MAIN_SELECTOR + + " " + + RIGHT_PANEL_WRAPPER_SELECTOR + + " " + + NATIVE_SCROLL_AREA_SELECTOR, ); - if (nativeScroll) nativeScroll.style.display = ''; + if (nativeScroll) nativeScroll.style.display = ""; const ourContent = document.getElementById(TAB_CONTENT_ID); if (ourContent) ourContent.remove(); @@ -1214,13 +1745,13 @@ function deactivateCuckooTab() { // Делегирование клика: ловим клики по вкладкам (наши и родные). document.addEventListener( - 'click', + "click", (e) => { const target = e.target; if (!target || !target.closest) return; // Наш таб - const ourBtn = target.closest('#' + TAB_BUTTON_ID); + const ourBtn = target.closest("#" + TAB_BUTTON_ID); if (ourBtn) { e.preventDefault(); e.stopPropagation(); @@ -1234,7 +1765,7 @@ document.addEventListener( deactivateCuckooTab(); } }, - true // capture + true, // capture ); /** @@ -1245,15 +1776,15 @@ document.addEventListener( */ function applyTabActiveStyle(btn, active) { if (!btn) return; - const bg = btn.querySelector('.ds-button__background'); + const bg = btn.querySelector(".ds-button__background"); if (active) { - btn.style.background = 'rgba(139,147,255,0.25)'; - btn.style.color = '#fff'; - if (bg) bg.style.background = 'rgba(139,147,255,0.25)'; + btn.style.background = "rgba(139,147,255,0.25)"; + btn.style.color = "#fff"; + if (bg) bg.style.background = "rgba(139,147,255,0.25)"; } else { - btn.style.background = ''; - btn.style.color = ''; - if (bg) bg.style.background = ''; + btn.style.background = ""; + btn.style.color = ""; + if (bg) bg.style.background = ""; } } @@ -1264,7 +1795,7 @@ function applyTabActiveStyle(btn, active) { function setTabActive() { const btn = document.getElementById(TAB_BUTTON_ID); if (!btn) return; - btn.setAttribute('data-cuckoo-active', '1'); + btn.setAttribute("data-cuckoo-active", "1"); applyTabActiveStyle(btn, true); } @@ -1275,7 +1806,7 @@ function setTabActive() { function setTabInactive() { const btn = document.getElementById(TAB_BUTTON_ID); if (!btn) return; - btn.removeAttribute('data-cuckoo-active'); + btn.removeAttribute("data-cuckoo-active"); applyTabActiveStyle(btn, false); } @@ -1285,19 +1816,21 @@ function setTabInactive() { function injectSettingsTab() { const container = document.querySelector(CUCKOO_TAB_BUTTON_SELECTOR); if (!container) return; - if (container.querySelector('#' + TAB_BUTTON_ID)) return; // уже вставлено + if (container.querySelector("#" + TAB_BUTTON_ID)) return; // уже вставлено - const btn = document.createElement('div'); + const btn = document.createElement("div"); btn.id = TAB_BUTTON_ID; - btn.setAttribute('role', 'button'); - btn.setAttribute('tabindex', '0'); - btn.className = 'ds-button ds-button--outlinedNeutral ds-button--borderless ds-button--capsule ds-button--m ds-button--icon-relative-m ds-button--min-width'; - btn.style.cssText = '--dsl-button-text-color: var(--dsw-alias-label-primary);' + - '--dsl-button-padding: 0 10px 0 8px;' + - '--dsl-button-border-radius: 12px;' + - '--dsl-button-icon-gap: 8px;' + - '--dsl-button-color-hover: var(--dsw-alias-interactive-bg-hover);' + - '--dsl-button-text-color-hover: var(--dsw-alias-label-primary);'; + btn.setAttribute("role", "button"); + btn.setAttribute("tabindex", "0"); + btn.className = + "ds-button ds-button--outlinedNeutral ds-button--borderless ds-button--capsule ds-button--m ds-button--icon-relative-m ds-button--min-width"; + btn.style.cssText = + "--dsl-button-text-color: var(--dsw-alias-label-primary);" + + "--dsl-button-padding: 0 10px 0 8px;" + + "--dsl-button-border-radius: 12px;" + + "--dsl-button-icon-gap: 8px;" + + "--dsl-button-color-hover: var(--dsw-alias-interactive-bg-hover);" + + "--dsl-button-text-color-hover: var(--dsw-alias-label-primary);"; btn.innerHTML = '
' + @@ -1305,8 +1838,8 @@ function injectSettingsTab() { ' ' + ' ' + ' ' + - ' ' + - '' + + " " + + "" + 'Cookie Code'; container.appendChild(btn); @@ -1332,7 +1865,7 @@ function start() { let emptyCycles = 0; let timer = null; const tick = () => { - const hasTab = !!document.querySelector('.d316d158'); + const hasTab = !!document.querySelector(".d316d158"); if (hasTab) { emptyCycles = 0; injectSettingsTab(); @@ -1347,4 +1880,11 @@ function start() { timer = setTimeout(tick, 800); } -module.exports = { start, injectSettingsTab, activateCuckooTab, deactivateCuckooTab, setTabActive, setTabInactive }; +module.exports = { + start, + injectSettingsTab, + activateCuckooTab, + deactivateCuckooTab, + setTabActive, + setTabInactive, +}; From fd502aad6d14a68e002fba169dabf6a2697d22e4 Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Thu, 17 Sep 2026 14:57:12 +0300 Subject: [PATCH 4/4] Update CHANGELOG.md --- CHANGELOG.md | 50 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 047ac08..9578c52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # Changelog +## [4.0.26] - 2026-09-17 + +### Added + +- **Telegram-бот: полноценный i18n (ru/en)** — все сообщения, меню и подписи бота переведены и вынесены в новый модуль `botsrc/i18n.js`: + - язык бота хранится отдельно от языка UI (ключ `telegramLanguage` в `cuckoo-settings.json`), по умолчанию — системный; + - при первом `/start` бот показывает выбор языка (кнопки 🇷🇺 Русский / 🇬🇧 English); + - меню команд `/` в Telegram регистрируется через `setMyCommands` на выбранном языке. +- **Telegram-бот: приём вложений** — можно отправить боту фото или документ, он скачает файл и прикрепит его в поле ввода активного чата: + - скачивание через `getFile` → `api.telegram.org/file/bot/` во временную папку `userData/tgtemp`; + - изображения вставляются как картинка (clipboard + `Ctrl+V`, как у `read_photo`), остальные файлы — через `DataTransfer` + `change`; + - MIME определяется по расширению, временный файл удаляется после прикрепления. +- **Telegram-бот: список сессий и переключение** — команды `/sessions` (постраничный список сессий проекта) и `/switch ` для переключения активной сессии. +- **Telegram-бот: пагинация длинных списков** — `/sessions`, `/log`, `/todos`, `/diff` разбиты на страницы по 10 элементов с кнопками «Назад / Вперёд». +- **Telegram-бот: новая команда `/todos`** — список задач активного окна с иконками статуса (`☐` / `◔` / `☑`) и пагинацией. +- **Telegram-бот: ограничение по User ID** — новая настройка `telegramAllowedUserId`: если задана, бот принимает сообщения только от этого пользователя (в дополнение к проверке `chat_id`). +- **Telegram-бот: игнор-лист уведомлений** — новая настройка `telegramToolNotifyIgnore` (список имён tool по одному в строке), уведомления о которых не отправляются. По умолчанию исключены «шумные» `read`, `readLines`, `glob`, `grep`, `todoWrite`. +- **Настройки: секция языка бота** — в меню `/settings` Telegram-бота добавлена отдельная страница «Язык» с выбором ru/en. + +### Changed + +- **Telegram-бот: меню настроек переведено на i18n и переструктурировано** — страницы (`ui`, `glass`, `agent`, `tg`, `lang`) собираются из локализованных ключей `label.*` / `settings.*`; лейблы кнопок и значения форматируются через `_t()`. +- **Рефакторинг `settings-tab.js`** — единый стиль (двойные кавычки, форматирование), переработаны привязки полей Telegram-настроек (добавлены `allowedUserId` и `notifyIgnore`), без изменения пользовательского поведения. +- **`botsrc/telegram.js`** — `configure()` принимает третий параметр `allowedUserId`; добавлены методы `setMyCommands()` и `downloadFile()`; long-polling пропускает сообщения без текста/фото/документа. +- **`settings-store.js`** — новые дефолты: `telegramLanguage`, `telegramAllowedUserId`, `telegramToolNotifyIgnore`; `telegramLanguage` подтягивается из системного языка, если пользователь его не выбирал. + +### Fixed + +- **Настройки Telegram: конфликт языка** — язык бота больше не перезаписывается языком UI; смена языка UI не влияет на язык бота (и наоборот). + +### Removed + +- Удалён вывод старых версий из CHANGELOG (оставлены только 4.0.26 и 4.0.25). + ## [4.0.25] - 2026-09-17 ### Added @@ -19,19 +53,3 @@ - валидация и тосты переписаны под новые единицы. - **i18n для настроек задержки** — сообщения об ошибках и сохранении переведены на ru/en (ранее были захардкожены на китайском): ключи `overlay.delay.*`. - Исправлена вёрстка строки задержки в overlay — подписи «до» и «сек» больше не переносятся по буквам (отдельный класс `.cuckoo-delay-label` вместо `.cuckoo-blur-label` с `space-between`). - -## [4.0.24] - 2026-09-16 - -### Added - -- **Цветные иконки файлов (Material Icon Theme)** — рядом с именами файлов теперь рисуется иконка по типу файла (расширению/имени): - - в чипах путей внутри ответов AI; - - в блоке «Затронуто» под ответом; - - в меню @-упоминаний файлов. - Иконки берутся из пакета `material-icon-theme` (~1377 расширений, ~2135 спец-имён) через IPC `get-file-icons`; неизвестные типы показывают контурный документ. -- **Регистронезависимый @-поиск файлов** — `@lua` находит `Sample.LUA` и наоборот (в ripgrep для `@`-поиска используется `--iglob` вместо регистрозависимого `--glob`). - -### Changed - -- **Меню slash-команд `/`** оставлено без иконок — иконки показываются только в списке файлов (`@`). -- Список файлов в меню `@` выровнен по левому краю с иконкой 16×16 (папка — SVG, файл — Material-иконка).