diff --git a/.gitignore b/.gitignore index 0487949..aa8ae16 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ test/coverage.lcov # Playwright MCP .playwright-mcp/ +/tiktok +/.cuckoo/skills diff --git a/CHANGELOG.md b/CHANGELOG.md index 4888db7..047ac08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [4.0.25] - 2026-09-17 + +### Added + +- **Инструмент `attach_file` — загрузка файлов-вложений в чат** — AI может прикрепить локальный файл (PDF, Word, Excel, PPT, изображения и др. бинарные форматы) прямо в поле ввода: + - скрытый `input[type=file]` на странице чата заполняется через `DataTransfer` + событие `change`; + - лимит 30 МБ, авто-определение MIME по расширению (~50 типов, фолбэк `application/octet-stream`); + - JS-API `attachFile(filePath)` (в песочнице также принимает объект `{ filePath }`), объявление в `cuckoo-tools.d.ts`, запись в `tool-names.js`; + - окно определяется через callback `attachFile` (по аналогии с `pasteImage` у `read_photo`), а не через `BrowserWindow.fromId`. +- **Telegram-бот: индикатор «печатает…»** — во время генерации ответа AI в чат отправляется `sendChatAction('typing')` каждые ~4 секунды, статус гасится по завершении стрима. IPC-хэндлеры `telegram-typing-start` / `telegram-typing-stop`, обновлён `observer.js`. + +### Changed + +- **Задержка отправки — теперь в секундах** — в overlay-панели поля ввода переведены с миллисекунд на секунды (шаг 0.1): + - хранение в `localStorage` и `state` осталось в мс (обратная совместимость с `chat-input.js`); + - конвертация мс ↔ сек на границе UI, подпись `сек` / `sec`; + - валидация и тосты переписаны под новые единицы. +- **i18n для настроек задержки** — сообщения об ошибках и сохранении переведены на ru/en (ранее были захардкожены на китайском): ключи `overlay.delay.*`. +- Исправлена вёрстка строки задержки в overlay — подписи «до» и «сек» больше не переносятся по буквам (отдельный класс `.cuckoo-delay-label` вместо `.cuckoo-blur-label` с `space-between`). + ## [4.0.24] - 2026-09-16 ### Added @@ -15,82 +35,3 @@ - **Меню slash-команд `/`** оставлено без иконок — иконки показываются только в списке файлов (`@`). - Список файлов в меню `@` выровнен по левому краю с иконкой 16×16 (папка — SVG, файл — Material-иконка). - -## [4.0.20] - 2026-09-16 - -### Added - -- **Telegram-бот: команды управления** — теперь из Telegram доступны: - - `/status` — статус окна, проекта, процессов, задач и polling бота - - `/stop` (синоним `/kill`) — прервать текущую задачу и убить активные дочерние процессы - - `/new` — открыть новый чат на домашней странице провайдера - - `/diff` — прислать `git diff` текущего проекта (до 3 КБ суммарно, с картой файлов) - - `/diagnostics` (синоним `/diag`) — отчёт диагностики интеграции main/preload/сайта -- **Telegram-бот: очередь сообщений** — исходящие сообщения идут через очередь с троттлингом ~1 сообщение/сек на чат (уважает лимит Telegram) -- **Telegram-бот: retry на 429** — при rate-limit соблюдается `retry_after` из ответа Telegram, до 3 попыток на запрос -- **Telegram-бот: файловый лог** — `/telegram-bot.log` с ротацией при 512 КБ (переименование в `.1`), уровни info/warn/error; дублирует вывод в консоль -- **Telegram-бот: маскировка токена** — все сообщения и логи проходят через `TelegramBot.maskToken()`, токен заменяется на `bot` -- **`/cancel` отменяет и подтверждения** — теперь сбрасывает не только ожидающий ввод, но и все pending-approval вызовы инструментов -- **`/settings`** — обновлена справка `/help` со всеми новыми командами -- **IPC `new-chat`** — новый обработчик + `window.electronAPI.newChat()` (переход на домашнюю страницу активного провайдера) -- **`window.electronAPI.runDiagnosticsText()`** — запуск диагностики в контексте страницы, возвращает готовый текстовый отчёт -- **Сайт проекта на GitHub Pages** (`docs/`) — лендинг в Material Design 3: - - тёмная тема, голубые акценты, Material Symbols Outlined - - версии на русском (`index.html`) и английском (`en.html`) с переключателем языка - - галерея скриншотов с lightbox (клик / Esc) - - секции: hero, «зачем», возможности, таблица инструментов, установка, использование, конфигурация, CTA - - готов к публикации через Settings → Pages → `/docs` -- **prettier** добавлен как devDependency - -## [4.0.19] - 2026-09-15 - -### Added - -- **Учёт токенов диалога**: панель «Токены диалога» с оценкой всего диалога из DOM (CJK 0.6 / ASCII 0.3), сохранение по `sessionId` в localStorage, обновление раз в секунду, на `popstate`/`hashchange` и `beforeunload`; формат K/M вместо китайского «万» -- **Токены под ответом AI**: оценка токенов именно этого ответа под каждым сообщением -- **Перехват серверных токенов DeepSeek** (`token-interceptor`): точные значения usage из ответов DeepSeek + настройка видимости -- **What's New**: модальное окно нововведений при первом запуске после обновления - - `src/main/changelog-parser.js` — парсер CHANGELOG.md (диапазон версий, semver-сравнение) - - `src/main/whats-new.js` — логика check-and-mark, атомарная запись состояния - - `src/preload/dom/whats-new.js` — одноразовый модал в рендерере; CHANGELOG.md включается в сборку -- **Диагностика интеграции** (`src/preload/dom/diagnostics.js`) — инструмент и UI проверки связки main/preload/сайт - -### Changed - -- **Удалён Claude-провайдер** (`src/providers/claude.js`, промпт, лого, тесты) -- Удалён мёртвый код network-hook (`src/interceptor/`, `intercept-observer.js`, `serverTokenUsage`, `useIntercept`) -- Обновлён UI вкладки настроек (`settings-tab.js`) -- Убран устаревший хелпер tryOpenInVSCode - -## [4.0.18] - 2026-09-14 - -### Added - -- **Авто-форматтеры** после `write`/`edit`: файл автоматически приводится к стилю проекта - - Встроенный реестр: prettier, biome, gofmt, ruff, rustfmt, shfmt, clang-format - - Определение по расширению + наличию конфига + бинарю в `PATH` или `node_modules/.bin` - - Отключается флагом `formattersEnabled: false` в `cuckoo-settings.json` - - Ошибки форматтера никогда не ломают `write`/`edit` - - Кнопка вкл/выкл в настройках Cookie Code; документация в README (EN + RU) -- **Режим плана (Plan Mode)**: кнопка-тумблер, per-session режим, перетаскиваемый диалог плана, инструмент `exit_plan_mode` -- **Produced files** под ответом AI (факт-ориентированно, только успешные операции), ru/en i18n, настройка `showProducedFiles`, тесты -- **Бейджи +/− строк** по каждому блоку diff в ответе -- **Telegram-бот**: команды `/help` и `/settings` с полным меню настроек - -### Fixed - -- `meta.produced` label: «Создано» → «Затронуто» / Produced → Affected -- i18n overlay: `OVERLAY_HTML` → `buildOverlayHTML()` — пересборка при смене языка - -## [4.0.17] - 2026-09-14 - -### Added - -- **File-path chips**: кликабельные чипы путей (абсолютный + относительный через `projectDir`) с открытием в VS Code / проводнике и настройкой видимости -- **Produced files** под ответом AI (базовый вариант) + ru/en i18n -- Override QR-кода скачивания приложения DeepSeek — свой QR и ссылка на Telegram -- `safe()`-обёртка для защиты от изменений DOM DeepSeek - -### Fixed - -- `meta.produced` label: Создано → Затронуто / Produced → Affected diff --git a/botsrc/index.js b/botsrc/index.js index b283439..7cbe5eb 100644 --- a/botsrc/index.js +++ b/botsrc/index.js @@ -8,25 +8,25 @@ * - notifyToolResult(toolName, ok, detail) → уведомление в TG (diff-стиль для edit). * - входящее сообщение из TG → sendToChat в активном окне DeepSeek. */ -const { telegramBot, TelegramBot } = require('./telegram'); -const settingsStore = require('../src/main/settings-store'); -const windowState = require('../src/main/window'); +const { telegramBot, TelegramBot } = require("./telegram"); +const settingsStore = require("../src/main/settings-store"); +const windowState = require("../src/main/window"); let started = false; // ==================== Файловый лог ==================== -const fs = require('fs'); -const path = require('path'); +const fs = require("fs"); +const path = require("path"); let _logFile = null; const LOG_MAX_BYTES = 512 * 1024; // 512 KB, затем ротация в .1 function _getLogFile() { if (_logFile) return _logFile; try { - const { app } = require('electron'); - _logFile = path.join(app.getPath('userData'), 'telegram-bot.log'); + const { app } = require("electron"); + _logFile = path.join(app.getPath("userData"), "telegram-bot.log"); } catch (_) { - _logFile = path.join(__dirname, 'telegram-bot.log'); + _logFile = path.join(__dirname, "telegram-bot.log"); } return _logFile; } @@ -36,10 +36,10 @@ function _appendLogFile(line) { const file = _getLogFile(); try { if (fs.existsSync(file) && fs.statSync(file).size > LOG_MAX_BYTES) { - fs.renameSync(file, file + '.1'); + fs.renameSync(file, file + ".1"); } } catch (_) {} - fs.appendFileSync(file, line + '\n', 'utf-8'); + fs.appendFileSync(file, line + "\n", "utf-8"); } catch (_) {} } @@ -47,26 +47,36 @@ function _read() { const s = settingsStore.readSettings(); return { enabled: !!s.telegramEnabled, - token: s.telegramBotToken || '', - chatId: s.telegramChatId || '', + token: s.telegramBotToken || "", + chatId: s.telegramChatId || "", notifyTools: !!s.telegramNotifyTools, chatFeed: !!s.telegramChatFeed, - approvalMode: s.toolApprovalMode || 'off', + approvalMode: s.toolApprovalMode || "off", }; } function _log(level, ...args) { - const tag = '[Cookie Code][telegram]'; - const token = (() => { try { return settingsStore.readSettings().telegramBotToken; } catch (_) { return ''; } })(); - const safe = args.map((a) => (typeof a === 'string' ? TelegramBot.maskToken(a, token) : a)); - if (level === 'error') console.error(tag, ...safe); - else if (level === 'warn') console.warn(tag, ...safe); + const tag = "[Cookie Code][telegram]"; + const token = (() => { + try { + return settingsStore.readSettings().telegramBotToken; + } catch (_) { + return ""; + } + })(); + const safe = args.map((a) => + typeof a === "string" ? TelegramBot.maskToken(a, token) : a, + ); + if (level === "error") console.error(tag, ...safe); + else if (level === "warn") console.warn(tag, ...safe); else console.log(tag, ...safe); // Дублируем в файл для отладки (уровни warn/error + info). try { const ts = new Date().toISOString(); - const text = safe.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' '); - _appendLogFile(ts + ' [' + level + '] ' + text); + const text = safe + .map((a) => (typeof a === "string" ? a : JSON.stringify(a))) + .join(" "); + _appendLogFile(ts + " [" + level + "] " + text); } catch (_) {} } @@ -78,7 +88,8 @@ function _log(level, ...args) { async function _sendToChat(text) { try { const win = windowState.getMainWindow(); - if (!win || win.isDestroyed()) return { success: false, error: 'нет активного окна' }; + if (!win || win.isDestroyed()) + return { success: false, error: "нет активного окна" }; const safe = JSON.stringify(String(text)); const script = `(function(){ @@ -95,12 +106,24 @@ async function _sendToChat(text) { // Небольшая пауза, чтобы React обработал ввод. await new Promise((r) => setTimeout(r, 150)); // Отправляем нативный Enter (тот же приём, что chat-send-enter). - win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Return', key: 'Enter' }); - win.webContents.sendInputEvent({ type: 'char', keyCode: 'Return', key: '\r' }); - win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Return', key: 'Enter' }); + win.webContents.sendInputEvent({ + type: "keyDown", + keyCode: "Return", + key: "Enter", + }); + win.webContents.sendInputEvent({ + type: "char", + keyCode: "Return", + key: "\r", + }); + win.webContents.sendInputEvent({ + type: "keyUp", + keyCode: "Return", + key: "Enter", + }); return { success: true }; } catch (err) { - _log('error', 'sendToChat error:', err.message); + _log("error", "sendToChat error:", err.message); return { success: false, error: err.message }; } } @@ -108,7 +131,8 @@ async function _sendToChat(text) { /** Выполнить JS в активном окне и вернуть результат. */ async function _evalInWindow(script) { const win = windowState.getMainWindow(); - if (!win || win.isDestroyed()) return { success: false, error: 'нет активного окна' }; + if (!win || win.isDestroyed()) + return { success: false, error: "нет активного окна" }; try { const result = await win.webContents.executeJavaScript(script, true); return { success: true, result }; @@ -140,11 +164,11 @@ function _activeSenderId() { /** Текст со списком задач (для /todos и уведомления о завершении). */ function formatTodos(todos) { const items = Array.isArray(todos) ? todos : []; - if (items.length === 0) return '☑ Список задач пуст.'; - 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'); + if (items.length === 0) return "☑ Список задач пуст."; + 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"); } /** @@ -166,15 +190,15 @@ let _onQuestionAnswered = null; /** Установить колбэк, вызываемый при ответе на вопрос из TG. */ function setOnQuestionAnswered(fn) { - _onQuestionAnswered = typeof fn === 'function' ? fn : null; + _onQuestionAnswered = typeof fn === "function" ? fn : null; } /** Экранирование для HTML в тексте вопроса. */ function _qEsc(s) { - return String(s == null ? '' : s) - .replace(/&/g, '&') - .replace(//g, '>'); + return String(s == null ? "" : s) + .replace(/&/g, "&") + .replace(//g, ">"); } /** @@ -186,20 +210,26 @@ function _qEsc(s) { */ async function askQuestion(requestId, questions) { const cfg = _read(); - if (!cfg.enabled || !cfg.token || !cfg.chatId) return { success: false, skipped: true }; + if (!cfg.enabled || !cfg.token || !cfg.chatId) + return { success: false, skipped: true }; const items = Array.isArray(questions) ? questions : []; if (items.length === 0) return { success: false, skipped: true }; - const lines = ['❓ Вопрос от ИИ']; + const lines = ["❓ Вопрос от ИИ"]; items.forEach((q, qi) => { - lines.push(''); - lines.push((qi + 1) + '. ' + _qEsc(q.question)); + lines.push(""); + lines.push(qi + 1 + ". " + _qEsc(q.question)); }); - lines.push(''); - lines.push('Выберите вариант кнопкой ниже.'); - const text = lines.join('\n'); - - const entry = { questions: items, answers: new Array(items.length).fill(null), messageId: null, tokens: [] }; + lines.push(""); + lines.push("Выберите вариант кнопкой ниже."); + const text = lines.join("\n"); + + const entry = { + questions: items, + answers: new Array(items.length).fill(null), + messageId: null, + tokens: [], + }; _pendingQuestions.set(requestId, entry); // Клавиатура: по строке на каждый вариант каждого вопроса. @@ -209,16 +239,16 @@ async function askQuestion(requestId, questions) { const opts = Array.isArray(q.options) ? q.options : []; entry.tokens[qi] = []; opts.forEach((o, oi) => { - const label = String(o.label || '').slice(0, 60); - const token = 't' + (++_cbTokenCounter); + const label = String(o.label || "").slice(0, 60); + const token = "t" + ++_cbTokenCounter; _callbackTokens.set(token, { requestId, qi, oi }); entry.tokens[qi][oi] = token; - keyboard.push([{ text: (qi + 1) + ') ' + label, callback_data: token }]); + keyboard.push([{ text: qi + 1 + ") " + label, callback_data: token }]); }); }); const res = await telegramBot.sendMessage(text, { - parseMode: 'HTML', + parseMode: "HTML", replyMarkup: { inline_keyboard: keyboard }, }); if (!res.success) { @@ -231,14 +261,14 @@ async function askQuestion(requestId, questions) { /** Обработать нажатие inline-кнопки с ответом на вопрос. */ async function _handleCallback(chatId, data, cbq) { - const token = String(data || ''); + const token = String(data || ""); // Меню настроек /settings — обрабатываем раньше других, т.к. префикс 'st_'. - if (token.startsWith('st_')) { + if (token.startsWith("st_")) { try { await _handleSettingsCallback(token, cbq); } catch (err) { - _log('error', 'settings callback error:', err.message); + _log("error", "settings callback error:", err.message); } return; } @@ -257,19 +287,25 @@ async function _handleCallback(chatId, data, cbq) { const entry = _pendingQuestions.get(requestId); if (!entry || entry.answers[qi] != null) { _callbackTokens.delete(token); - await telegramBot.answerCallbackQuery(cbq.id, { text: 'Вопрос уже закрыт' }); + await telegramBot.answerCallbackQuery(cbq.id, { + text: "Вопрос уже закрыт", + }); return; } const q = entry.questions[qi]; const opt = (q.options || [])[oi]; if (!opt) { - await telegramBot.answerCallbackQuery(cbq.id, { text: 'Вариант не найден' }); + await telegramBot.answerCallbackQuery(cbq.id, { + text: "Вариант не найден", + }); return; } - entry.answers[qi] = { question: q.question, answer: String(opt.label || '') }; - await telegramBot.answerCallbackQuery(cbq.id, { text: 'Принято: ' + String(opt.label || '').slice(0, 40) }); + entry.answers[qi] = { question: q.question, answer: String(opt.label || "") }; + await telegramBot.answerCallbackQuery(cbq.id, { + text: "Принято: " + String(opt.label || "").slice(0, 40), + }); // Обновляем сообщение: показываем выбранные ответы, убираем использованные кнопки. _refreshQuestionMessage(requestId, entry); @@ -278,11 +314,13 @@ async function _handleCallback(chatId, data, cbq) { if (entry.answers.every((a) => a && a.answer)) { _pendingQuestions.delete(requestId); for (const row of entry.tokens) { - for (const t of (row || [])) _callbackTokens.delete(t); + for (const t of row || []) _callbackTokens.delete(t); } - if (typeof _onQuestionAnswered === 'function') { - try { _onQuestionAnswered(requestId, entry.answers.slice()); } catch (err) { - _log('error', 'onQuestionAnswered error:', err.message); + if (typeof _onQuestionAnswered === "function") { + try { + _onQuestionAnswered(requestId, entry.answers.slice()); + } catch (err) { + _log("error", "onQuestionAnswered error:", err.message); } } } @@ -291,36 +329,49 @@ async function _handleCallback(chatId, data, cbq) { /** Отправить запрос подтверждения tool-вызова в Telegram. */ async function requestApproval(requestId, info) { const cfg = _read(); - if (!cfg.enabled || !cfg.token || !cfg.chatId || cfg.approvalMode === 'off') { + if (!cfg.enabled || !cfg.token || !cfg.chatId || cfg.approvalMode === "off") { return { success: false, skipped: true }; } - const id = String(requestId || ''); - if (!id) return { success: false, error: 'requestId не задан' }; - - const isJs = info && info.kind === 'js'; - const name = isJs ? 'JS-скрипт' : String((info && info.toolName) || 'инструмент'); - let details = ''; + const id = String(requestId || ""); + if (!id) return { success: false, error: "requestId не задан" }; + + const isJs = info && info.kind === "js"; + const name = isJs + ? "JS-скрипт" + : String((info && info.toolName) || "инструмент"); + let details = ""; try { details = isJs - ? String((info && info.code) || '') + ? String((info && info.code) || "") : JSON.stringify((info && info.params) || {}, null, 2); - } catch (_) { details = ''; } + } catch (_) { + details = ""; + } const body = escapeHtml(details.slice(0, 3000)); - const token = 'a' + (++_cbTokenCounter); + const token = "a" + ++_cbTokenCounter; const entry = { messageId: null, token, resolve: null }; - const promise = new Promise((resolve) => { entry.resolve = resolve; }); + const promise = new Promise((resolve) => { + entry.resolve = resolve; + }); _pendingApprovals.set(id, entry); _approvalCallbackTokens.set(token, { requestId: id, approved: true }); - const msg = '🔐 Подтверждение команды\n' + escapeHtml(name) + '' + - (body ? '\n
' + body + '
' : '') + - '\nРазрешить выполнение?'; + const msg = + "🔐 Подтверждение команды\n" + + escapeHtml(name) + + "" + + (body ? "\n
" + body + "
" : "") + + "\nРазрешить выполнение?"; const res = await telegramBot.sendMessage(msg, { - parseMode: 'HTML', - replyMarkup: { inline_keyboard: [[ - { text: '✅ Разрешить', callback_data: token }, - { text: '❌ Отклонить', callback_data: token + 'd' }, - ]] }, + parseMode: "HTML", + replyMarkup: { + inline_keyboard: [ + [ + { text: "✅ Разрешить", callback_data: token }, + { text: "❌ Отклонить", callback_data: token + "d" }, + ], + ], + }, }); if (!res.success) { _pendingApprovals.delete(id); @@ -328,55 +379,62 @@ async function requestApproval(requestId, info) { return res; } entry.messageId = res.messageId || null; - _approvalCallbackTokens.set(token + 'd', { requestId: id, approved: false }); + _approvalCallbackTokens.set(token + "d", { requestId: id, approved: false }); return promise; } async function _handleApprovalCallback(ref, cbq) { const entry = _pendingApprovals.get(ref.requestId); if (!entry) { - await telegramBot.answerCallbackQuery(cbq.id, { text: 'Запрос уже закрыт' }); + await telegramBot.answerCallbackQuery(cbq.id, { + text: "Запрос уже закрыт", + }); return; } _pendingApprovals.delete(ref.requestId); _approvalCallbackTokens.delete(entry.token); - _approvalCallbackTokens.delete(entry.token + 'd'); + _approvalCallbackTokens.delete(entry.token + "d"); await telegramBot.answerCallbackQuery(cbq.id, { - text: ref.approved ? 'Команда разрешена' : 'Команда отклонена', + text: ref.approved ? "Команда разрешена" : "Команда отклонена", }); if (entry.messageId) { - const status = ref.approved ? '✅ Разрешено' : '❌ Отклонено'; - await telegramBot.editMessageText(entry.messageId, '🔐 Подтверждение команды\n' + status, { - parseMode: 'HTML', - replyMarkup: { inline_keyboard: [] }, - }); + const status = ref.approved ? "✅ Разрешено" : "❌ Отклонено"; + await telegramBot.editMessageText( + entry.messageId, + "🔐 Подтверждение команды\n" + status, + { + parseMode: "HTML", + replyMarkup: { inline_keyboard: [] }, + }, + ); } - if (typeof entry.resolve === 'function') { + if (typeof entry.resolve === "function") { entry.resolve({ success: true, approved: ref.approved }); } } /** Отменить запрос, если подтверждение уже получено в окне приложения. */ function cancelApproval(requestId) { - const id = String(requestId || ''); + const id = String(requestId || ""); const entry = _pendingApprovals.get(id); if (!entry) return { success: true, skipped: true }; _pendingApprovals.delete(id); _approvalCallbackTokens.delete(entry.token); - _approvalCallbackTokens.delete(entry.token + 'd'); - if (typeof entry.resolve === 'function') entry.resolve({ success: false, skipped: true }); + _approvalCallbackTokens.delete(entry.token + "d"); + if (typeof entry.resolve === "function") + entry.resolve({ success: false, skipped: true }); return { success: true }; } /** Перерисовать сообщение с вопросами: отметить выбранное, убрать лишние кнопки. */ async function _refreshQuestionMessage(requestId, entry) { if (!entry.messageId) return; - const lines = ['❓ Вопрос от ИИ']; + const lines = ["❓ Вопрос от ИИ"]; entry.questions.forEach((q, qi) => { - lines.push(''); + lines.push(""); const chosen = entry.answers[qi]; - lines.push((qi + 1) + '. ' + _qEsc(q.question)); - if (chosen) lines.push('✅ ' + _qEsc(chosen.answer)); + lines.push(qi + 1 + ". " + _qEsc(q.question)); + if (chosen) lines.push("✅ " + _qEsc(chosen.answer)); }); // Оставляем кнопки только для неотвеченных вопросов. @@ -385,16 +443,20 @@ async function _refreshQuestionMessage(requestId, entry) { if (entry.answers[qi]) return; const opts = Array.isArray(q.options) ? q.options : []; opts.forEach((o, oi) => { - const label = String(o.label || '').slice(0, 60); - const token = (entry.tokens[qi] && entry.tokens[qi][oi]) || ('t' + (++_cbTokenCounter)); - if (!_callbackTokens.has(token)) _callbackTokens.set(token, { requestId, qi, oi }); - keyboard.push([{ text: (qi + 1) + ') ' + label, callback_data: token }]); + const label = String(o.label || "").slice(0, 60); + const token = + (entry.tokens[qi] && entry.tokens[qi][oi]) || "t" + ++_cbTokenCounter; + if (!_callbackTokens.has(token)) + _callbackTokens.set(token, { requestId, qi, oi }); + keyboard.push([{ text: qi + 1 + ") " + label, callback_data: token }]); }); }); - await telegramBot.editMessageText(entry.messageId, lines.join('\n'), { - parseMode: 'HTML', - replyMarkup: keyboard.length ? { inline_keyboard: keyboard } : { inline_keyboard: [] }, + await telegramBot.editMessageText(entry.messageId, lines.join("\n"), { + parseMode: "HTML", + replyMarkup: keyboard.length + ? { inline_keyboard: keyboard } + : { inline_keyboard: [] }, }); } @@ -406,67 +468,185 @@ async function _refreshQuestionMessage(requestId, entry) { const SETTINGS_PAGES = { root: { - title: '⚙️ Настройки Cookie Code', - text: 'Что настраиваем?', + title: "⚙️ Настройки Cookie Code", + text: "Что настраиваем?", items: [ - { goto: 'ui', label: '🎨 Интерфейс' }, - { goto: 'glass', label: '🪟 Стекло и панель' }, - { goto: 'agent', label: '🤖 Агент и приватность' }, - { goto: 'tg', label: '📡 Telegram-бот' }, + { goto: "ui", label: "🎨 Интерфейс" }, + { goto: "glass", label: "🪟 Стекло и панель" }, + { goto: "agent", label: "🤖 Агент и приватность" }, + { goto: "tg", label: "📡 Telegram-бот" }, ], }, ui: { - title: '🎨 Интерфейс', - text: 'Общие визуальные эффекты.', + title: "🎨 Интерфейс", + text: "Общие визуальные эффекты.", items: [ - { key: 'customizationEnabled', label: 'Кастомизация вкл', type: 'bool' }, - { key: 'rgbUsername', label: 'RGB-переливание ника', type: 'bool' }, - { key: 'language', label: 'Язык UI', type: 'enum', values: [['ru', '🇷🇺 RU'], ['en', '🇬🇧 EN']] }, + { key: "customizationEnabled", label: "Кастомизация вкл", type: "bool" }, + { key: "rgbUsername", label: "RGB-переливание ника", type: "bool" }, + { + key: "language", + label: "Язык UI", + type: "enum", + values: [ + ["ru", "🇷🇺 RU"], + ["en", "🇬🇧 EN"], + ], + }, ], back: true, }, glass: { - title: '🪟 Стекло и панель', - text: 'Прозрачность, размытие и цвета панели Cookie Code.', + title: "🪟 Стекло и панель", + text: "Прозрачность, размытие и цвета панели Cookie Code.", items: [ - { key: 'backgroundBlur', label: 'Размытие фона', type: 'number', step: 2, min: 0, max: 30, unit: 'px' }, - { key: 'headerBlur', label: 'Размытие шапки', type: 'number', step: 2, min: 0, max: 30, unit: 'px' }, - { key: 'sidebarBlur', label: 'Размытие сайдбара', type: 'number', step: 2, min: 0, max: 30, unit: 'px' }, - { key: 'headerOpacity', label: 'Прозрачность шапки', type: 'number', step: 5, min: 0, max: 100, unit: '%' }, - { key: 'sidebarOpacity', label: 'Прозрачность сайдбара', type: 'number', step: 5, min: 0, max: 100, unit: '%' }, - { key: 'toolBlockOpacity', label: 'Прозрачность tool-блоков', type: 'number', step: 5, min: 0, max: 100, unit: '%' }, - { key: 'toolBlockBlur', label: 'Размытие tool-блоков', type: 'number', step: 2, min: 0, max: 30, unit: 'px' }, - { key: 'overlayOpacity', label: 'Прозрачность панели', type: 'number', step: 5, min: 0, max: 100, unit: '%' }, - { key: 'overlayBlur', label: 'Размытие панели', type: 'number', step: 2, min: 0, max: 30, unit: 'px' }, - { key: 'overlayWidth', label: 'Ширина панели', type: 'number', step: 20, min: 200, max: 600, unit: 'px' }, - { key: 'overlayBtnRadius', label: 'Скругление кнопок', type: 'number', step: 2, min: 0, max: 24, unit: 'px' }, - { key: 'overlayBgColor', label: 'Цвет подложки', type: 'color' }, - { key: 'overlayPrimaryColor', label: 'Акцентный цвет', type: 'color' }, + { + key: "backgroundBlur", + label: "Размытие фона", + type: "number", + step: 2, + min: 0, + max: 30, + unit: "px", + }, + { + key: "headerBlur", + label: "Размытие шапки", + type: "number", + step: 2, + min: 0, + max: 30, + unit: "px", + }, + { + key: "sidebarBlur", + label: "Размытие сайдбара", + type: "number", + step: 2, + min: 0, + max: 30, + unit: "px", + }, + { + key: "headerOpacity", + label: "Прозрачность шапки", + type: "number", + step: 5, + min: 0, + max: 100, + unit: "%", + }, + { + key: "sidebarOpacity", + label: "Прозрачность сайдбара", + type: "number", + step: 5, + min: 0, + max: 100, + unit: "%", + }, + { + key: "toolBlockOpacity", + label: "Прозрачность tool-блоков", + type: "number", + step: 5, + min: 0, + max: 100, + unit: "%", + }, + { + key: "toolBlockBlur", + label: "Размытие tool-блоков", + type: "number", + step: 2, + min: 0, + max: 30, + unit: "px", + }, + { + key: "overlayOpacity", + label: "Прозрачность панели", + type: "number", + step: 5, + min: 0, + max: 100, + unit: "%", + }, + { + key: "overlayBlur", + label: "Размытие панели", + type: "number", + step: 2, + min: 0, + max: 30, + unit: "px", + }, + { + key: "overlayWidth", + label: "Ширина панели", + type: "number", + step: 20, + min: 200, + max: 600, + unit: "px", + }, + { + key: "overlayBtnRadius", + label: "Скругление кнопок", + type: "number", + step: 2, + min: 0, + max: 24, + unit: "px", + }, + { key: "overlayBgColor", label: "Цвет подложки", type: "color" }, + { key: "overlayPrimaryColor", label: "Акцентный цвет", type: "color" }, ], back: true, }, agent: { - title: '🤖 Агент и приватность', - text: 'Подтверждения, служебные сообщения, форматтеры.', + title: "🤖 Агент и приватность", + text: "Подтверждения, служебные сообщения, форматтеры.", items: [ - { key: 'toolApprovalMode', label: 'Подтверждение tools', type: 'enum', values: [['off', '⚪ Off'], ['risky', '🟡 Risky'], ['all', '🔴 All']] }, - { key: 'hideSystemMessages', label: 'Скрывать служебные сообщения', type: 'bool' }, - { key: 'formattersEnabled', label: 'Авто-форматтеры', type: 'bool' }, - { key: 'fileChipEnabled', label: 'Пути как чипы', type: 'bool' }, - { key: 'showProducedFiles', label: 'Затронутые файлы', type: 'bool' }, - { key: 'dangerousPatterns', label: 'Опасные паттерны', type: 'multiline' }, + { + key: "toolApprovalMode", + label: "Подтверждение tools", + type: "enum", + values: [ + ["off", "⚪ Off"], + ["risky", "🟡 Risky"], + ["all", "🔴 All"], + ], + }, + { + key: "hideSystemMessages", + label: "Скрывать служебные сообщения", + type: "bool", + }, + { key: "formattersEnabled", label: "Авто-форматтеры", type: "bool" }, + { key: "fileChipEnabled", label: "Пути как чипы", type: "bool" }, + { key: "showProducedFiles", label: "Затронутые файлы", type: "bool" }, + { + key: "dangerousPatterns", + label: "Опасные паттерны", + type: "multiline", + }, ], back: true, }, tg: { - title: '📡 Telegram-бот', - text: 'Управление ботом и уведомлениями.', + title: "📡 Telegram-бот", + text: "Управление ботом и уведомлениями.", items: [ - { key: 'telegramEnabled', label: 'Бот включён', type: 'bool' }, - { key: 'telegramNotifyTools', label: 'Уведомления о tool', type: 'bool' }, - { key: 'telegramChatFeed', label: 'Принимать из TG', type: 'bool' }, - { key: 'telegramBotToken', label: 'Токен бота', type: 'text', secret: true }, - { key: 'telegramChatId', label: 'Chat ID', type: 'text' }, + { key: "telegramEnabled", label: "Бот включён", type: "bool" }, + { key: "telegramNotifyTools", label: "Уведомления о tool", type: "bool" }, + { key: "telegramChatFeed", label: "Принимать из TG", type: "bool" }, + { + key: "telegramBotToken", + label: "Токен бота", + type: "text", + secret: true, + }, + { key: "telegramChatId", label: "Chat ID", type: "text" }, ], back: true, }, @@ -479,104 +659,127 @@ function _findSetting(key) { for (const name of Object.keys(SETTINGS_PAGES)) { const page = SETTINGS_PAGES[name]; if (!page.items) continue; - for (const it of page.items) if (it.key === key) return { page: name, item: it }; + for (const it of page.items) + if (it.key === key) return { page: name, item: it }; } return null; } function _getVal(key) { - try { return settingsStore.readSettings()[key]; } catch (_) { return undefined; } + try { + return settingsStore.readSettings()[key]; + } catch (_) { + return undefined; + } } function _setVal(key, value) { - try { return !!settingsStore.setSetting(key, value); } catch (err) { - _log('error', 'setSetting error:', err.message); + try { + return !!settingsStore.setSetting(key, value); + } catch (err) { + _log("error", "setSetting error:", err.message); return false; } } -function _esc(s) { return escapeHtml(s); } +function _esc(s) { + return escapeHtml(s); +} function _formatValue(item, value) { - if (item.type === 'bool') return value ? '✅ ВКЛ' : '⚪ ВЫКЛ'; - if (item.type === 'enum') { + if (item.type === "bool") return value ? "✅ ВКЛ" : "⚪ ВЫКЛ"; + if (item.type === "enum") { const found = (item.values || []).find(([v]) => v === value); return found ? found[1] : String(value); } - if (item.type === 'number') return String(value) + (item.unit || ''); - if (item.type === 'color') return String(value || ''); - if (item.type === 'text' || item.type === 'multiline') { - if (item.secret) return value ? '••••' + String(value).slice(-4) : '(пусто)'; - const s = String(value == null ? '' : value); - return s.length > 20 ? s.slice(0, 20) + '…' : (s || '(пусто)'); + if (item.type === "number") return String(value) + (item.unit || ""); + if (item.type === "color") return String(value || ""); + if (item.type === "text" || item.type === "multiline") { + if (item.secret) + return value ? "••••" + String(value).slice(-4) : "(пусто)"; + const s = String(value == null ? "" : value); + return s.length > 20 ? s.slice(0, 20) + "…" : s || "(пусто)"; } return String(value); } function _renderPage(pageName) { const page = SETTINGS_PAGES[pageName]; - if (!page) return _renderPage('root'); - const lines = [page.title, '', page.text]; + if (!page) return _renderPage("root"); + const lines = [page.title, "", page.text]; const keyboard = []; if (page.items) { for (const item of page.items) { const v = _getVal(item.key); - if (item.type === 'bool') { - const icon = v ? '✅' : '⚪'; - keyboard.push([{ text: icon + ' ' + item.label, callback_data: 'st_t_' + item.key }]); - } else if (item.type === 'enum') { + if (item.type === "bool") { + const icon = v ? "✅" : "⚪"; + keyboard.push([ + { text: icon + " " + item.label, 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' }]); + keyboard.push([ + { text: item.label + ": " + cur, callback_data: "st_noop" }, + ]); const opts = (item.values || []).map(([val, lbl]) => ({ text: lbl, - callback_data: 'st_s_' + item.key + '__' + val, + callback_data: "st_s_" + item.key + "__" + val, })); if (opts.length) keyboard.push(opts); - } else if (item.type === 'number') { + } else if (item.type === "number") { const cur = _formatValue(item, v); keyboard.push([ - { text: '➖', callback_data: 'st_n_' + item.key + '_-' }, - { text: item.label + ': ' + cur, callback_data: 'st_noop' }, - { text: '➕', callback_data: 'st_n_' + item.key + '_+' }, + { text: "➖", callback_data: "st_n_" + item.key + "_-" }, + { text: item.label + ": " + cur, callback_data: "st_noop" }, + { text: "➕", callback_data: "st_n_" + item.key + "_+" }, ]); - } else if (item.type === 'color') { + } else if (item.type === "color") { keyboard.push([ - { text: '✏️ ' + item.label, callback_data: 'st_e_' + item.key }, - { text: String(v || ''), callback_data: 'st_noop' }, + { text: "✏️ " + item.label, callback_data: "st_e_" + item.key }, + { text: String(v || ""), callback_data: "st_noop" }, ]); - } else if (item.type === 'text' || item.type === 'multiline') { + } else if (item.type === "text" || item.type === "multiline") { keyboard.push([ - { text: '✏️ ' + item.label + ': ' + _formatValue(item, v), callback_data: 'st_e_' + item.key }, + { + text: "✏️ " + item.label + ": " + _formatValue(item, v), + callback_data: "st_e_" + item.key, + }, ]); } } } if (page.back) { - keyboard.push([{ text: '⬅️ Назад', callback_data: 'st_page_root' }]); + keyboard.push([{ text: "⬅️ Назад", callback_data: "st_page_root" }]); } else { keyboard.push([ - { text: '🔧 Агент', callback_data: 'st_page_agent' }, - { text: '🎨 UI', callback_data: 'st_page_ui' }, + { text: "🔧 Агент", callback_data: "st_page_agent" }, + { text: "🎨 UI", callback_data: "st_page_ui" }, ]); keyboard.push([ - { text: '🪟 Стекло', callback_data: 'st_page_glass' }, - { text: '📡 Telegram', callback_data: 'st_page_tg' }, + { text: "🪟 Стекло", callback_data: "st_page_glass" }, + { text: "📡 Telegram", callback_data: "st_page_tg" }, ]); - keyboard.push([{ text: '🔄 Обновить', callback_data: 'st_page_root' }]); + keyboard.push([{ text: "🔄 Обновить", callback_data: "st_page_root" }]); } - return { text: lines.join('\n'), keyboard: { inline_keyboard: keyboard } }; + return { text: lines.join("\n"), keyboard: { inline_keyboard: keyboard } }; } async function _showSettingsMenu(chatId, pageName, messageId) { const state = _settingsState.get(chatId) || {}; - const page = SETTINGS_PAGES[pageName] ? pageName : 'root'; + const page = SETTINGS_PAGES[pageName] ? pageName : "root"; state.page = page; _settingsState.set(chatId, state); const { text, keyboard } = _renderPage(page); if (messageId) { - return telegramBot.editMessageText(messageId, text, { parseMode: 'HTML', replyMarkup: keyboard }); + return telegramBot.editMessageText(messageId, text, { + parseMode: "HTML", + replyMarkup: keyboard, + }); } - const res = await telegramBot.sendMessage(text, { parseMode: 'HTML', replyMarkup: keyboard }); + const res = await telegramBot.sendMessage(text, { + parseMode: "HTML", + replyMarkup: keyboard, + }); if (res.success && res.messageId) { state.msgId = res.messageId; _settingsState.set(chatId, state); @@ -587,73 +790,107 @@ async function _showSettingsMenu(chatId, pageName, messageId) { async function _handleSettingsCallback(data, cbq) { const chatId = String(cbq.message && cbq.message.chat && cbq.message.chat.id); const msgId = cbq.message && cbq.message.message_id; - const state = _settingsState.get(chatId) || { page: 'root' }; + const state = _settingsState.get(chatId) || { page: "root" }; - if (data.startsWith('st_page_')) { + if (data.startsWith("st_page_")) { await telegramBot.answerCallbackQuery(cbq.id); - await _showSettingsMenu(chatId, data.slice('st_page_'.length), msgId); + await _showSettingsMenu(chatId, data.slice("st_page_".length), msgId); return; } - if (data.startsWith('st_t_')) { - const key = data.slice('st_t_'.length); + if (data.startsWith("st_t_")) { + const key = data.slice("st_t_".length); const next = !_getVal(key); const ok = _setVal(key, next); - await telegramBot.answerCallbackQuery(cbq.id, { text: ok ? (next ? 'Включено' : 'Выключено') : 'Ошибка' }); - if (ok) { try { await applySettings(); } catch (_) {} } + await telegramBot.answerCallbackQuery(cbq.id, { + text: ok ? (next ? "Включено" : "Выключено") : "Ошибка", + }); + if (ok) { + try { + await applySettings(); + } catch (_) {} + } await _showSettingsMenu(chatId, state.page, msgId); return; } - if (data.startsWith('st_s_')) { - const rest = data.slice('st_s_'.length); - const sep = rest.indexOf('__'); + if (data.startsWith("st_s_")) { + const rest = data.slice("st_s_".length); + const sep = rest.indexOf("__"); const key = rest.slice(0, sep); const val = rest.slice(sep + 2); const ok = _setVal(key, val); - await telegramBot.answerCallbackQuery(cbq.id, { text: ok ? 'Сохранено' : 'Ошибка' }); - if (ok && (key === 'language')) { try { await applySettings(); } catch (_) {} } + await telegramBot.answerCallbackQuery(cbq.id, { + text: ok ? "Сохранено" : "Ошибка", + }); + if (ok && key === "language") { + try { + await applySettings(); + } catch (_) {} + } await _showSettingsMenu(chatId, state.page, msgId); return; } - if (data.startsWith('st_n_')) { - const rest = data.slice('st_n_'.length); - const sep = rest.lastIndexOf('_'); + if (data.startsWith("st_n_")) { + const rest = data.slice("st_n_".length); + const sep = rest.lastIndexOf("_"); const key = rest.slice(0, sep); const sign = rest.slice(sep + 1); const found = _findSetting(key); - if (!found) { await telegramBot.answerCallbackQuery(cbq.id); return; } + if (!found) { + await telegramBot.answerCallbackQuery(cbq.id); + return; + } const item = found.item; const step = item.step || 1; let val = Number(_getVal(key)); if (!Number.isFinite(val)) val = 0; - val += sign === '+' ? step : -step; - if (typeof item.min === 'number') val = Math.max(item.min, val); - if (typeof item.max === 'number') val = Math.min(item.max, val); + val += sign === "+" ? step : -step; + if (typeof item.min === "number") val = Math.max(item.min, val); + 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 || '')) : 'Ошибка' }); + await telegramBot.answerCallbackQuery(cbq.id, { + text: ok ? String(val) + (item.unit || "") : "Ошибка", + }); await _showSettingsMenu(chatId, state.page, msgId); return; } - if (data.startsWith('st_e_')) { - const key = data.slice('st_e_'.length); + if (data.startsWith("st_e_")) { + const key = data.slice("st_e_".length); const found = _findSetting(key); - if (!found) { await telegramBot.answerCallbackQuery(cbq.id); return; } + if (!found) { + await telegramBot.answerCallbackQuery(cbq.id); + return; + } _settingsWaiting.set(chatId, { key }); - await telegramBot.answerCallbackQuery(cbq.id, { text: 'Отправьте новое значение' }); + await telegramBot.answerCallbackQuery(cbq.id, { + text: "Отправьте новое значение", + }); const cur = _getVal(key); - const hint = found.item.type === 'multiline' - ? 'Отправьте новый список — по одному паттерну в строке. /cancel — отмена.' - : 'Отправьте новое значение одним сообщением. /cancel — отмена.'; - const curText = found.item.type === 'multiline' - ? (Array.isArray(cur) ? cur.join('\n') : String(cur || '')) - : String(cur == null ? '' : cur); + const hint = + found.item.type === "multiline" + ? "Отправьте новый список — по одному паттерну в строке. /cancel — отмена." + : "Отправьте новое значение одним сообщением. /cancel — отмена."; + const curText = + found.item.type === "multiline" + ? Array.isArray(cur) + ? cur.join("\n") + : String(cur || "") + : String(cur == null ? "" : cur); await telegramBot.sendMessage( - '✏️ ' + _esc(found.item.label) + '\n' + - hint + '\n\nТекущее:\n
' + _esc(curText.slice(0, 1500)) + '
', - { parseMode: 'HTML' } + "✏️ " + + _esc(found.item.label) + + "\n" + + hint + + "\n\nТекущее:\n
" +
+        _esc(curText.slice(0, 1500)) +
+        "
", + { parseMode: "HTML" }, ); return; } - if (data === 'st_noop') { await telegramBot.answerCallbackQuery(cbq.id); return; } + if (data === "st_noop") { + await telegramBot.answerCallbackQuery(cbq.id); + return; + } await telegramBot.answerCallbackQuery(cbq.id); } @@ -662,82 +899,126 @@ async function _cmdStop() { let count = 0; try { const win = windowState.getMainWindow(); - if (win && !win.isDestroyed() && win.webContents && !win.webContents.isDestroyed()) { - try { win.webContents.stop(); } catch (_) {} + if ( + win && + !win.isDestroyed() && + win.webContents && + !win.webContents.isDestroyed() + ) { + try { + win.webContents.stop(); + } catch (_) {} } - const { processManager } = require('../src/main/process-manager'); + const { processManager } = require("../src/main/process-manager"); const r = await processManager.killAll(); count = r.count || 0; } catch (err) { - _log('error', '/stop error:', err.message); - return telegramBot.sendMessage('❌ Не удалось остановить: ' + escapeHtml(err.message)); + _log("error", "/stop error:", err.message); + return telegramBot.sendMessage( + "❌ Не удалось остановить: " + escapeHtml(err.message), + ); } - return telegramBot.sendMessage('🛑 Остановлено. Убито процессов: ' + count + '.', { parseMode: 'HTML' }); + return telegramBot.sendMessage( + "🛑 Остановлено. Убито процессов: " + count + ".", + { parseMode: "HTML" }, + ); } /** /status — окно, проект, процессы, версия, polling. */ async function _cmdStatus() { const cfg = _read(); - const lines = ['📊 Статус Cookie Code', '']; + const lines = ["📊 Статус Cookie Code", ""]; - let version = '?'; - try { version = require('electron').app.getVersion(); } catch (_) {} - lines.push('📦 Версия: ' + escapeHtml(version) + ''); + let version = "?"; + try { + version = require("electron").app.getVersion(); + } catch (_) {} + lines.push("📦 Версия: " + escapeHtml(version) + ""); const win = windowState.getMainWindow(); const winOk = !!(win && !win.isDestroyed()); - lines.push('🪟 Окно: ' + (winOk ? 'активно' : '❌ нет')); + lines.push("🪟 Окно: " + (winOk ? "активно" : "❌ нет")); const ctx = _activeContext(); - const projectDir = ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; - lines.push('📁 Проект: ' + (projectDir ? '' + escapeHtml(projectDir) + '' : 'не выбран')); - if (ctx && ctx.providerId) lines.push('🌐 Провайдер: ' + escapeHtml(ctx.providerId)); + const projectDir = + ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; + lines.push( + "📁 Проект: " + + (projectDir + ? "" + escapeHtml(projectDir) + "" + : "не выбран"), + ); + if (ctx && ctx.providerId) + lines.push("🌐 Провайдер: " + escapeHtml(ctx.providerId)); let procCount = 0; try { - const { processManager } = require('../src/main/process-manager'); - procCount = (processManager.activeProcesses && processManager.activeProcesses.size) || 0; + const { processManager } = require("../src/main/process-manager"); + procCount = + (processManager.activeProcesses && processManager.activeProcesses.size) || + 0; } catch (_) {} - lines.push('⚙️ Активных процессов: ' + procCount + ''); + lines.push("⚙️ Активных процессов: " + procCount + ""); - let todos = 0, done = 0; + let todos = 0, + done = 0; try { const sid = _activeSenderId(); if (sid != null) { - const list = require('../src/main/todo-store').getList(sid) || []; + const list = require("../src/main/todo-store").getList(sid) || []; todos = list.length; - done = list.filter((t) => t.status === 'completed').length; + done = list.filter((t) => t.status === "completed").length; } } catch (_) {} - lines.push('☑️ Задачи: ' + done + '/' + todos); + lines.push("☑️ Задачи: " + done + "/" + todos); const st = telegramBot.getStatus(); - lines.push('📡 Бот: ' + (cfg.enabled ? (st.polling ? '✅ работает' : '⚠️ включён, но не опрашивает') : '⚪ выключен')); - if (st.lastError) lines.push('⚠️ Последняя ошибка: ' + escapeHtml(String(st.lastError).slice(0, 200))); + lines.push( + "📡 Бот: " + + (cfg.enabled + ? st.polling + ? "✅ работает" + : "⚠️ включён, но не опрашивает" + : "⚪ выключен"), + ); + if (st.lastError) + lines.push( + "⚠️ Последняя ошибка: " + escapeHtml(String(st.lastError).slice(0, 200)), + ); - return telegramBot.sendMessage(lines.join('\n'), { parseMode: 'HTML' }); + return telegramBot.sendMessage(lines.join("\n"), { parseMode: "HTML" }); } /** /new — новый чат (очистка контекста текущей сессии). */ async function _cmdNew() { - const res = await _evalInWindow('(async () => { try { const r = await window.electronAPI.newChat(); return r; } catch (e) { return { success: false, error: e.message }; } })()'); - if (!res.success) return telegramBot.sendMessage('⚠️ ' + escapeHtml(res.error)); + const res = await _evalInWindow( + "(async () => { try { const r = await window.electronAPI.newChat(); return r; } catch (e) { return { success: false, error: e.message }; } })()", + ); + if (!res.success) + return telegramBot.sendMessage("⚠️ " + escapeHtml(res.error)); const inner = res.result || {}; - if (inner.success === false) return telegramBot.sendMessage('⚠️ ' + escapeHtml(inner.error || 'не удалось')); - return telegramBot.sendMessage('🆕 Новый чат открыт.'); + if (inner.success === false) + return telegramBot.sendMessage( + "⚠️ " + escapeHtml(inner.error || "не удалось"), + ); + return telegramBot.sendMessage("🆕 Новый чат открыт."); } /** /diff — показать текущие изменения (git diff) в проекте. */ async function _cmdDiff() { const ctx = _activeContext(); - const projectDir = ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; - if (!projectDir) return telegramBot.sendMessage('⚠️ Проект не выбран.'); + const projectDir = + ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; + if (!projectDir) return telegramBot.sendMessage("⚠️ Проект не выбран."); - const gitDiff = require('../src/main/git-diff'); + const gitDiff = require("../src/main/git-diff"); const status = await gitDiff.getStatus(projectDir); - if (!status.success) return telegramBot.sendMessage('⚠️ ' + escapeHtml(status.reason || 'git недоступен')); + if (!status.success) + return telegramBot.sendMessage( + "⚠️ " + escapeHtml(status.reason || "git недоступен"), + ); const files = status.files || []; - if (files.length === 0) return telegramBot.sendMessage('✅ Изменений нет.'); + if (files.length === 0) return telegramBot.sendMessage("✅ Изменений нет."); const chunks = []; let total = 0; @@ -747,54 +1028,66 @@ async function _cmdDiff() { const d = await gitDiff.getFileDiff(projectDir, f.path, f.status); if (!d.success || !d.diff) continue; const text = d.diff.slice(0, MAX_TOTAL - total); - chunks.push('===== ' + f.path + ' =====\n' + text); + chunks.push("===== " + f.path + " =====\n" + text); total += text.length; } - const header = '📝 Изменения (' + files.length + ')\n'; - const fileList = files.map((f) => '• ' + escapeHtml(f.path)).join('\n'); - const body = chunks.join('\n\n') || '(diff недоступен)'; - const msg = header + '
' + escapeHtml(fileList) + '
\n
' + escapeHtml(body) + '
'; - return telegramBot.sendMessage(msg, { parseMode: 'HTML' }); + const header = "📝 Изменения (" + files.length + ")\n"; + const fileList = files.map((f) => "• " + escapeHtml(f.path)).join("\n"); + const body = chunks.join("\n\n") || "(diff недоступен)"; + const msg = + header + + "
" + + escapeHtml(fileList) + + "
\n
" +
+    escapeHtml(body) +
+    "
"; + return telegramBot.sendMessage(msg, { parseMode: "HTML" }); } /** /diagnostics — отчёт диагностики интеграции. */ async function _cmdDiagnostics() { - const res = await _evalInWindow('(async () => { try { return await window.electronAPI.runDiagnosticsText(); } catch (e) { return "ERROR: " + e.message; } })()'); - if (!res.success) return telegramBot.sendMessage('⚠️ ' + escapeHtml(res.error)); - const text = String(res.result || '(пусто)').slice(0, 3500); - return telegramBot.sendMessage('🩺 Диагностика\n
' + escapeHtml(text) + '
', { parseMode: 'HTML' }); + const res = await _evalInWindow( + '(async () => { try { return await window.electronAPI.runDiagnosticsText(); } catch (e) { return "ERROR: " + e.message; } })()', + ); + if (!res.success) + return telegramBot.sendMessage("⚠️ " + escapeHtml(res.error)); + const text = String(res.result || "(пусто)").slice(0, 3500); + return telegramBot.sendMessage( + "🩺 Диагностика\n
" + escapeHtml(text) + "
", + { parseMode: "HTML" }, + ); } async function _cmdHelp() { const text = [ - '🦆 Cookie Code — помощь', - '', - 'Команды', - '/settings — открыть меню настроек', - '/status — статус: окно, проект, процессы', - '/stop — прервать задачу и убить процессы', - '/new — новый чат', - '/diff — показать изменения (git diff)', - '/diagnostics — диагностика интеграции', - '/todos — список задач активного окна', - '/cancel — отменить ввод / подтверждение', - '/help — эта справка', - '', - 'Возможности', - '• Уведомления о вызовах инструментов', - '• Приём входящих сообщений в чат DeepSeek', - '• Подтверждение команд — кнопки «Разрешить / Отклонить»', - '• Вопросы от AI с вариантами ответа', - '', - 'Настройки синхронизированы с десктопным приложением.', - ].join('\n'); - return telegramBot.sendMessage(text, { parseMode: 'HTML' }); + "🦆 Cookie Code — помощь", + "", + "Команды", + "/settings — открыть меню настроек", + "/status — статус: окно, проект, процессы", + "/stop — прервать задачу и убить процессы", + "/new — новый чат", + "/diff — показать изменения (git diff)", + "/diagnostics — диагностика интеграции", + "/todos — список задач активного окна", + "/cancel — отменить ввод / подтверждение", + "/help — эта справка", + "", + "Возможности", + "• Уведомления о вызовах инструментов", + "• Приём входящих сообщений в чат DeepSeek", + "• Подтверждение команд — кнопки «Разрешить / Отклонить»", + "• Вопросы от AI с вариантами ответа", + "", + "Настройки синхронизированы с десктопным приложением.", + ].join("\n"); + return telegramBot.sendMessage(text, { parseMode: "HTML" }); } async function _cmdSettings(chatId) { - _settingsState.set(chatId, { page: 'root' }); - return _showSettingsMenu(chatId, 'root'); + _settingsState.set(chatId, { page: "root" }); + return _showSettingsMenu(chatId, "root"); } async function _handleSettingsInput(chatId, text) { @@ -802,67 +1095,89 @@ async function _handleSettingsInput(chatId, text) { if (!waiting) return false; const { key } = waiting; const found = _findSetting(key); - if (!found) { _settingsWaiting.delete(chatId); return false; } + if (!found) { + _settingsWaiting.delete(chatId); + return false; + } _settingsWaiting.delete(chatId); const item = found.item; let value = text; - if (item.type === 'multiline') { - value = text.split(/\r?\n/).map((s) => s.trim()).filter(Boolean); - } else if (item.type === 'color') { + if (item.type === "multiline") { + value = text + .split(/\r?\n/) + .map((s) => s.trim()) + .filter(Boolean); + } 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( + "⚠ Некорректный цвет. Ожидается hex вида #8b93ff.", + { parseMode: "HTML" }, + ); return true; } - } else if (item.type === 'text') { + } else if (item.type === "text") { value = text.trim(); } const ok = _setVal(key, value); - if (ok && (key === 'telegramBotToken' || key === 'telegramChatId' || key === 'telegramEnabled')) { - try { await applySettings(); } catch (_) {} + if ( + ok && + (key === "telegramBotToken" || + key === "telegramChatId" || + key === "telegramEnabled") + ) { + try { + await applySettings(); + } catch (_) {} } - await telegramBot.sendMessage(ok ? ('✅ Сохранено: ' + _esc(item.label) + '') : '❌ Не удалось сохранить', { parseMode: 'HTML' }); + await telegramBot.sendMessage( + ok + ? "✅ Сохранено: " + _esc(item.label) + "" + : "❌ Не удалось сохранить", + { parseMode: "HTML" }, + ); const state = _settingsState.get(chatId); - if (state && state.msgId) await _showSettingsMenu(chatId, state.page || 'root', state.msgId); + if (state && state.msgId) + await _showSettingsMenu(chatId, state.page || "root", state.msgId); return true; } /** Входящее из TG → в чат DeepSeek (или команда). */ async function _handleIncoming(chatId, text) { const cfg = _read(); - const raw = String(text || ''); + const raw = String(text || ""); const cmd = raw.trim().toLowerCase(); // ---- Служебные команды ---- - if (cmd === '/help' || cmd === '/start') { + if (cmd === "/help" || cmd === "/start") { await _cmdHelp(); return; } - if (cmd === '/settings' || cmd === '/setting' || cmd === '/config') { + if (cmd === "/settings" || cmd === "/setting" || cmd === "/config") { await _cmdSettings(chatId); return; } - if (cmd === '/stop' || cmd === '/kill') { + if (cmd === "/stop" || cmd === "/kill") { await _cmdStop(); return; } - if (cmd === '/status' || cmd === '/stat') { + if (cmd === "/status" || cmd === "/stat") { await _cmdStatus(); return; } - if (cmd === '/new') { + if (cmd === "/new") { await _cmdNew(); return; } - if (cmd === '/diff') { + if (cmd === "/diff") { await _cmdDiff(); return; } - if (cmd === '/diagnostics' || cmd === '/diag') { + if (cmd === "/diagnostics" || cmd === "/diag") { await _cmdDiagnostics(); return; } - if (cmd === '/cancel') { + if (cmd === "/cancel") { const hadSettings = _settingsWaiting.delete(chatId); // Отменяем все ожидающие approval (AI больше не ждёт подтверждения). let cancelledApprovals = 0; @@ -870,22 +1185,25 @@ async function _handleIncoming(chatId, text) { const r = cancelApproval(id); if (r && r.success && !r.skipped) cancelledApprovals++; } - if (hadSettings) await telegramBot.sendMessage('✖️ Ввод отменён.'); - else if (cancelledApprovals > 0) await telegramBot.sendMessage('✖️ Отменено подтверждений: ' + cancelledApprovals); - else await telegramBot.sendMessage('Нечего отменять.'); + if (hadSettings) await telegramBot.sendMessage("✖️ Ввод отменён."); + else if (cancelledApprovals > 0) + await telegramBot.sendMessage( + "✖️ Отменено подтверждений: " + cancelledApprovals, + ); + else await telegramBot.sendMessage("Нечего отменять."); return; } // Команда /todos — показать список задач активного окна. - if (cmd === '/todos' || cmd === '/todo') { + if (cmd === "/todos" || cmd === "/todo") { let todos = []; try { const senderId = _activeSenderId(); if (senderId != null) { - const todoStore = require('../src/main/todo-store'); + const todoStore = require("../src/main/todo-store"); todos = todoStore.getList(senderId); } } catch (err) { - _log('error', '/todos error:', err.message); + _log("error", "/todos error:", err.message); } await telegramBot.sendMessage(formatTodos(todos)); return; @@ -899,25 +1217,35 @@ async function _handleIncoming(chatId, text) { // ---- Обычное сообщение ---- if (!cfg.chatFeed) { - _log('info', 'chatFeed выключен — игнор входящего:', text); + _log("info", "chatFeed выключен — игнор входящего:", text); return; } - _log('info', 'incoming → chat:', 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"), + ); } } /** Уведомить, что все задачи выполнены (со списком ниже). */ async function notifyAllDone(todos) { const cfg = _read(); - if (!cfg.enabled || !cfg.notifyTools) return { success: false, skipped: true }; + if (!cfg.enabled || !cfg.notifyTools) + return { success: false, skipped: true }; const items = Array.isArray(todos) ? 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
' + lines + '
'; - return telegramBot.sendMessage(msg, { parseMode: 'HTML' }); + const lines = items.map((t) => "☑ " + escapeHtml(t.content)).join("\n"); + const msg = + "🎉 Все задачи выполнены (" + + items.length + + "/" + + items.length + + ")\n
" + + lines + + "
"; + return telegramBot.sendMessage(msg, { parseMode: "HTML" }); } /** @@ -925,46 +1253,65 @@ async function notifyAllDone(todos) { * Для edit показываем НОВЫЙ код (new_string) до 2000 символов. */ function formatToolArgs(toolName, args) { - if (!args || typeof args !== 'object') return ''; + if (!args || typeof args !== "object") return ""; const a = args; switch (toolName) { - case 'edit': { - const file = a.file_path || a.path || ''; - const neu = a.new_string != null ? String(a.new_string) : ''; - const old = a.old_string != null ? String(a.old_string) : ''; - let s = ''; - if (file) s += '📄 ' + file + '\n'; - if (old) s += '➖ было:\n' + old.slice(0, 500) + (old.length > 500 ? '\n…(обрезано)' : '') + '\n'; - if (neu) s += '➕ стало:\n' + neu.slice(0, 2000) + (neu.length > 2000 ? '\n…(обрезано, всего ' + neu.length + ' симв.)' : ''); + case "edit": { + const file = a.file_path || a.path || ""; + const neu = a.new_string != null ? String(a.new_string) : ""; + const old = a.old_string != null ? String(a.old_string) : ""; + let s = ""; + if (file) s += "📄 " + file + "\n"; + if (old) + s += + "➖ было:\n" + + old.slice(0, 500) + + (old.length > 500 ? "\n…(обрезано)" : "") + + "\n"; + if (neu) + s += + "➕ стало:\n" + + neu.slice(0, 2000) + + (neu.length > 2000 + ? "\n…(обрезано, всего " + neu.length + " симв.)" + : ""); return s; } - case 'read': - case 'readLines': - case 'write': - return a.file_path || a.path ? '📄 ' + (a.file_path || a.path) : ''; - case 'grep': - return '🔎 ' + (a.pattern || '') + (a.path ? ' в ' + a.path : '') + (a.include ? ' (' + a.include + ')' : ''); - case 'glob': - return '🔎 ' + (a.pattern || '') + (a.path ? ' в ' + a.path : ''); - case 'bash': - case 'pwsh': - return '💻 ' + (a.command || ''); - case 'todoWrite': - return '☑ ' + (Array.isArray(a.todos) ? a.todos.length + ' пункт(ов)' : ''); + case "read": + case "readLines": + case "write": + return a.file_path || a.path ? "📄 " + (a.file_path || a.path) : ""; + case "grep": + return ( + "🔎 " + + (a.pattern || "") + + (a.path ? " в " + a.path : "") + + (a.include ? " (" + a.include + ")" : "") + ); + case "glob": + return "🔎 " + (a.pattern || "") + (a.path ? " в " + a.path : ""); + case "bash": + case "pwsh": + return "💻 " + (a.command || ""); + case "todoWrite": + return ( + "☑ " + (Array.isArray(a.todos) ? a.todos.length + " пункт(ов)" : "") + ); default: { // Общий случай — компактный JSON, кроме длинных полей. try { const shallow = {}; for (const k of Object.keys(a)) { const v = a[k]; - if (typeof v === 'string' && v.length > 200) shallow[k] = v.slice(0, 200) + '…'; + if (typeof v === "string" && v.length > 200) + shallow[k] = v.slice(0, 200) + "…"; else shallow[k] = v; } const s = JSON.stringify(shallow); - return s && s !== '{}' ? '📥 ' + s : ''; + return s && s !== "{}" ? "📥 " + s : ""; } catch (_) { - return ''; + return ""; } } } @@ -972,55 +1319,75 @@ function formatToolArgs(toolName, args) { /** Экранирование для HTML parse_mode. */ function escapeHtml(s) { - return String(s == null ? '' : s) - .replace(/&/g, '&') - .replace(//g, '>'); + return String(s == null ? "" : s) + .replace(/&/g, "&") + .replace(//g, ">"); } /** Уведомление о результате tool. */ async function notifyToolResult(toolName, ok, detail) { const cfg = _read(); - if (!cfg.enabled || !cfg.notifyTools) return { success: false, skipped: true }; - const emoji = ok ? '✅' : '❌'; - const header = emoji + ' ' + escapeHtml(toolName); + if (!cfg.enabled || !cfg.notifyTools) + return { success: false, skipped: true }; + const emoji = ok ? "✅" : "❌"; + const header = emoji + " " + escapeHtml(toolName); // detail может быть строкой (ошибка) или объектом { args, preview }. - if (detail && typeof detail === 'object') { + if (detail && typeof detail === "object") { const { args, preview } = detail; const argsText = formatToolArgs(toolName, args); const parts = []; - if (toolName === 'edit') { + if (toolName === "edit") { // Красивый diff-стиль: старый код как -, новый как +. - const file = (args && (args.file_path || args.path)) || ''; - const oldS = args && args.old_string != null ? String(args.old_string) : ''; - const newS = args && args.new_string != null ? String(args.new_string) : ''; + const file = (args && (args.file_path || args.path)) || ""; + const oldS = + args && args.old_string != null ? String(args.old_string) : ""; + const newS = + args && args.new_string != null ? String(args.new_string) : ""; const lines = []; const addLines = (prefix, s) => { - for (const ln of s.split('\n')) lines.push(prefix + escapeHtml(ln)); + for (const ln of s.split("\n")) lines.push(prefix + escapeHtml(ln)); }; - if (oldS) addLines('➖ ', oldS.slice(0, 1000)); - if (newS) addLines('➕ ', newS.slice(0, 2000)); - const body = lines.join('\n') || '(пустое изменение)'; - const title = file ? ' ' + escapeHtml(file) : ''; - const msg = emoji + ' edit' + title + '\n
' + body + '
'; - return telegramBot.sendMessage(msg, { parseMode: 'HTML' }); + if (oldS) addLines("➖ ", oldS.slice(0, 1000)); + if (newS) addLines("➕ ", newS.slice(0, 2000)); + const body = lines.join("\n") || "(пустое изменение)"; + const title = file ? " " + escapeHtml(file) : ""; + const msg = + emoji + + " edit" + + title + + "\n
" + + body + + "
"; + return telegramBot.sendMessage(msg, { parseMode: "HTML" }); } // Остальные инструменты: аргументы + результат в цитате
. const bodyParts = []; if (argsText) bodyParts.push(argsText); - if (preview) bodyParts.push('📤 ' + String(preview).replace(/\n{3,}/g, '\n\n').slice(0, 600)); - const bodyHtml = escapeHtml(bodyParts.join('\n')); - const msg = header + (bodyHtml ? '\n
' + bodyHtml + '
' : ''); - return telegramBot.sendMessage(msg, { parseMode: 'HTML' }); + if (preview) + bodyParts.push( + "📤 " + + String(preview) + .replace(/\n{3,}/g, "\n\n") + .slice(0, 600), + ); + const bodyHtml = escapeHtml(bodyParts.join("\n")); + const msg = + header + (bodyHtml ? "\n
" + bodyHtml + "
" : ""); + return telegramBot.sendMessage(msg, { parseMode: "HTML" }); } // detail — строка. let msg = header; - if (detail) msg += '\n
' + escapeHtml(String(detail).slice(0, 600)) + '
'; - return telegramBot.sendMessage(msg, { parseMode: 'HTML' }); + if (detail) + msg += + "\n
" + + escapeHtml(String(detail).slice(0, 600)) + + "
"; + return telegramBot.sendMessage(msg, { parseMode: "HTML" }); } /** Применить настройки: пересоздать конфиг бота и (при необходимости) запустить polling. */ @@ -1042,6 +1409,38 @@ function getStatus() { return Object.assign({ started }, telegramBot.getStatus()); } +// ==================== «ИИ печатает…» ==================== +// +// Telegram показывает статус «печатает…» ~5 секунд после sendChatAction('typing'). +// Пока идёт стриминг ответа, раз в 4 секунды продлеваем индикатор. +// startTyping() — включить, stopTyping() — выключить (по завершении генерации). + +let _typingTimer = null; + +function startTyping() { + const cfg = _read(); + if (!cfg.enabled || !cfg.token || !cfg.chatId) + return { success: false, skipped: true }; + telegramBot.configure(cfg.token, cfg.chatId); + if (_typingTimer) return { success: true, already: true }; + const tick = () => { + telegramBot.sendChatAction("typing").catch(() => {}); + }; + tick(); + _typingTimer = setInterval(tick, 4000); + return { success: true }; +} + +function stopTyping() { + // Telegram гасит «печатает…» сам через ~5с после последнего sendChatAction. + // Достаточно прекратить продление индикатора. + if (_typingTimer) { + clearInterval(_typingTimer); + _typingTimer = null; + } + return { success: true }; +} + /** * Отправить ответ AI в Telegram (для просмотра с телефона). * Обрезает слишком длинные ответы. @@ -1049,11 +1448,14 @@ function getStatus() { async function notifyAIResponse(text) { const cfg = _read(); if (!cfg.enabled || !cfg.chatFeed) return { success: false, skipped: true }; - const s = String(text || '').trim(); + const s = String(text || "").trim(); if (!s) return { success: false, skipped: true }; const MAX = 3500; - const body = s.length > MAX ? s.slice(0, MAX) + '\n…(обрезано, всего ' + s.length + ' симв.)' : s; - return telegramBot.sendMessage('🤖 ' + body); + const body = + s.length > MAX + ? s.slice(0, MAX) + "\n…(обрезано, всего " + s.length + " симв.)" + : s; + return telegramBot.sendMessage("🤖 " + body); } /** Пингануть бота (getMe) для проверки токена. */ @@ -1067,7 +1469,9 @@ async function ping() { async function testSend() { const cfg = _read(); telegramBot.configure(cfg.token, cfg.chatId); - return telegramBot.sendMessage('👋 Cookie Code: тестовое уведомление. Всё работает.'); + return telegramBot.sendMessage( + "👋 Cookie Code: тестовое уведомление. Всё работает.", + ); } module.exports = { @@ -1077,6 +1481,8 @@ module.exports = { testSend, notifyToolResult, notifyAIResponse, + startTyping, + stopTyping, notifyAllDone, requestApproval, cancelApproval, diff --git a/botsrc/telegram.js b/botsrc/telegram.js index d21cb27..881d368 100644 --- a/botsrc/telegram.js +++ b/botsrc/telegram.js @@ -14,44 +14,46 @@ * * Использует global fetch (Node 18+/Electron), без npm-зависимостей. */ -const API_BASE = 'https://api.telegram.org'; +const API_BASE = "https://api.telegram.org"; class TelegramBot { constructor() { - this.token = ''; - this.chatId = ''; + this.token = ""; + this.chatId = ""; this.polling = false; this.offset = 0; this.pollTimer = null; - this.onMessage = null; // (chatId, text, msg) => void - this.onCallback = null; // (chatId, data, cbq) => void - this.onLog = null; // (level, ...args) => void + this.onMessage = null; // (chatId, text, msg) => void + this.onCallback = null; // (chatId, data, cbq) => void + this.onLog = null; // (level, ...args) => void this._lastError = null; this._pollDelayMs = 2000; // пауза при ошибке/пустом ответе - this._queue = []; // очередь исходящих сообщений + this._queue = []; // очередь исходящих сообщений this._draining = false; - this._minGapMs = 1100; // ~1 msg/sec на чат (лимит Telegram) + this._minGapMs = 1100; // ~1 msg/sec на чат (лимит Telegram) } /** Замаскировать токен бота (bot) в произвольной строке. */ static maskToken(str, token) { - const s = String(str == null ? '' : str); + const s = String(str == null ? "" : str); if (!token) return s; - return s.split(token).join('bot'); + return s.split(token).join("bot"); } _log(level, ...args) { - if (typeof this.onLog !== 'function') return; + if (typeof this.onLog !== "function") return; const masked = args.map((a) => - typeof a === 'string' ? TelegramBot.maskToken(a, this.token) : a + typeof a === "string" ? TelegramBot.maskToken(a, this.token) : a, ); - try { this.onLog(level, ...masked); } catch (_) {} + try { + this.onLog(level, ...masked); + } catch (_) {} } /** Настроить токен/chat_id. Возвращает true, если параметры валидны. */ configure(token, chatId) { - this.token = String(token || '').trim(); - this.chatId = String(chatId || '').trim(); + this.token = String(token || "").trim(); + this.chatId = String(chatId || "").trim(); return this.isConfigured(); } @@ -60,7 +62,7 @@ class TelegramBot { } _apiUrl(method) { - return API_BASE + '/bot' + this.token + '/' + method; + return API_BASE + "/bot" + this.token + "/" + method; } /** @@ -71,27 +73,35 @@ class TelegramBot { * @returns {Promise<{success:boolean, status?:number, data?:object, error?:string}>} */ async _callApi(method, body, opts = {}) { - if (!this.token) return { success: false, error: 'token не задан' }; + if (!this.token) return { success: false, error: "token не задан" }; const attempts = opts.attempts || 3; for (let attempt = 0; attempt < attempts; attempt++) { try { const res = await fetch(this._apiUrl(method), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = await res.json().catch(() => ({})); // Rate limit: уважаем retry_after и повторяем. if (res.status === 429 || (data && data.error_code === 429)) { - const retryAfter = (data && data.parameters && data.parameters.retry_after) || 3; - this._log('warn', method + ': 429 rate limit, retry через ' + retryAfter + 'с'); + const retryAfter = + (data && data.parameters && data.parameters.retry_after) || 3; + this._log( + "warn", + method + ": 429 rate limit, retry через " + retryAfter + "с", + ); await this._sleep(retryAfter * 1000); continue; } if (!res.ok || !data.ok) { - return { success: false, status: res.status, error: (data && data.description) || ('HTTP ' + res.status) }; + return { + success: false, + status: res.status, + error: (data && data.description) || "HTTP " + res.status, + }; } return { success: true, status: res.status, data }; } catch (err) { @@ -102,7 +112,7 @@ class TelegramBot { return { success: false, error: err.message }; } } - return { success: false, error: 'rate limit: превышено число попыток' }; + return { success: false, error: "rate limit: превышено число попыток" }; } /** @@ -112,16 +122,18 @@ class TelegramBot { * @returns {Promise<{success: boolean, error?: string, messageId?: number}>} */ sendMessage(text, opts = {}) { - if (!this.token) return Promise.resolve({ success: false, error: 'token не задан' }); + if (!this.token) + return Promise.resolve({ success: false, error: "token не задан" }); const chatId = opts.chatId || this.chatId; - if (!chatId) return Promise.resolve({ success: false, error: 'chat_id не задан' }); + if (!chatId) + return Promise.resolve({ success: false, error: "chat_id не задан" }); return new Promise((resolve) => { this._queue.push({ - method: 'sendMessage', + method: "sendMessage", body: { chat_id: chatId, - text: String(text || ''), + text: String(text || ""), parse_mode: opts.parseMode || undefined, disable_web_page_preview: true, reply_markup: opts.replyMarkup || undefined, @@ -132,6 +144,23 @@ class TelegramBot { }); } + /** + * Отправить chat action (например 'typing'). Telegram показывает его ~5 секунд. + * @param {string} action — 'typing' | 'upload_photo' | 'record_video' | ... + * @param {{chatId?: string}} [opts] + * @returns {Promise<{success: boolean, error?: string}>} + */ + async sendChatAction(action, opts = {}) { + if (!this.token) return { success: false, error: "token не задан" }; + const chatId = opts.chatId || this.chatId; + if (!chatId) return { success: false, error: "chat_id не задан" }; + const r = await this._callApi("sendChatAction", { + chat_id: chatId, + action: String(action || "typing"), + }); + return r.success ? { success: true } : { success: false, error: r.error }; + } + /** Последовательно разгребает очередь с минимальным интервалом между запросами. */ async _drainQueue() { if (this._draining) return; @@ -141,7 +170,10 @@ class TelegramBot { const res = await this._callApi(job.method, job.body); if (res.success) { this._lastError = null; - job.resolve({ success: true, messageId: res.data.result && res.data.result.message_id }); + job.resolve({ + success: true, + messageId: res.data.result && res.data.result.message_id, + }); } else { this._lastError = res.error; job.resolve({ success: false, error: res.error }); @@ -158,39 +190,46 @@ class TelegramBot { * @returns {Promise<{success:boolean, messageId?:number, error?:string}>} */ async sendDocument(filePath, opts = {}) { - return this._sendFile('sendDocument', 'document', filePath, opts); + return this._sendFile("sendDocument", "document", filePath, opts); } /** Отправить фото в Telegram. */ async sendPhoto(filePath, opts = {}) { - return this._sendFile('sendPhoto', 'photo', filePath, opts); + return this._sendFile("sendPhoto", "photo", filePath, opts); } /** Общая реализация отправки файла через multipart/form-data. */ async _sendFile(method, field, filePath, opts = {}) { - if (!this.token) return { success: false, error: 'token не задан' }; + if (!this.token) return { success: false, error: "token не задан" }; const chatId = opts.chatId || this.chatId; - if (!chatId) return { success: false, error: 'chat_id не задан' }; + if (!chatId) return { success: false, error: "chat_id не задан" }; try { - const fs = require('fs'); - const path = require('path'); - if (!fs.existsSync(filePath)) return { success: false, error: 'файл не найден: ' + filePath }; + const fs = require("fs"); + const path = require("path"); + if (!fs.existsSync(filePath)) + return { success: false, error: "файл не найден: " + filePath }; const buf = fs.readFileSync(filePath); const form = new FormData(); - form.append('chat_id', String(chatId)); - if (opts.caption) form.append('caption', String(opts.caption)); + form.append("chat_id", String(chatId)); + if (opts.caption) form.append("caption", String(opts.caption)); const name = opts.fileName || path.basename(filePath); form.append(field, new Blob([buf]), name); - const res = await fetch(this._apiUrl(method), { method: 'POST', body: form }); + const res = await fetch(this._apiUrl(method), { + method: "POST", + body: form, + }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.ok) { - const err = (data && data.description) || ('HTTP ' + res.status); + const err = (data && data.description) || "HTTP " + res.status; this._lastError = err; return { success: false, error: err }; } this._lastError = null; - return { success: true, messageId: data.result && data.result.message_id }; + return { + success: true, + messageId: data.result && data.result.message_id, + }; } catch (err) { this._lastError = err.message; return { success: false, error: err.message }; @@ -203,7 +242,7 @@ class TelegramBot { * @param {{text?: string, showAlert?: boolean}} [opts] */ async answerCallbackQuery(callbackQueryId, opts = {}) { - const r = await this._callApi('answerCallbackQuery', { + const r = await this._callApi("answerCallbackQuery", { callback_query_id: callbackQueryId, text: opts.text || undefined, show_alert: !!opts.showAlert, @@ -218,13 +257,13 @@ class TelegramBot { * @param {{chatId?: string, parseMode?: string, replyMarkup?: object}} [opts] */ async editMessageText(messageId, text, opts = {}) { - if (!this.token) return { success: false, error: 'token не задан' }; + if (!this.token) return { success: false, error: "token не задан" }; const chatId = opts.chatId || this.chatId; - if (!chatId) return { success: false, error: 'chat_id не задан' }; - const r = await this._callApi('editMessageText', { + if (!chatId) return { success: false, error: "chat_id не задан" }; + const r = await this._callApi("editMessageText", { chat_id: chatId, message_id: messageId, - text: String(text || ''), + text: String(text || ""), parse_mode: opts.parseMode || undefined, disable_web_page_preview: true, reply_markup: opts.replyMarkup || undefined, @@ -236,12 +275,15 @@ class TelegramBot { * Короткий пинг бота: getMe. Полезно для проверки токена из UI. */ async getMe() { - if (!this.token) return { success: false, error: 'token не задан' }; + if (!this.token) return { success: false, error: "token не задан" }; try { - const res = await fetch(this._apiUrl('getMe')); + const res = await fetch(this._apiUrl("getMe")); const data = await res.json().catch(() => ({})); if (!res.ok || !data.ok) { - return { success: false, error: (data && data.description) || ('HTTP ' + res.status) }; + return { + success: false, + error: (data && data.description) || "HTTP " + res.status, + }; } return { success: true, username: data.result && data.result.username }; } catch (err) { @@ -257,15 +299,17 @@ class TelegramBot { startPolling(onMessage, onCallback) { if (this.polling) { // Обновляем обработчики на случай повторного старта. - this.onMessage = typeof onMessage === 'function' ? onMessage : this.onMessage; - this.onCallback = typeof onCallback === 'function' ? onCallback : this.onCallback; + this.onMessage = + typeof onMessage === "function" ? onMessage : this.onMessage; + this.onCallback = + typeof onCallback === "function" ? onCallback : this.onCallback; return { success: true, already: true }; } - if (!this.token) return { success: false, error: 'token не задан' }; - this.onMessage = typeof onMessage === 'function' ? onMessage : null; - this.onCallback = typeof onCallback === 'function' ? onCallback : null; + if (!this.token) return { success: false, error: "token не задан" }; + this.onMessage = typeof onMessage === "function" ? onMessage : null; + this.onCallback = typeof onCallback === "function" ? onCallback : null; this.polling = true; - this._log('info', 'Telegram polling запущен'); + this._log("info", "Telegram polling запущен"); this._pollLoop(); return { success: true }; } @@ -276,45 +320,53 @@ class TelegramBot { clearTimeout(this.pollTimer); this.pollTimer = null; } - this._log('info', 'Telegram polling остановлен'); + this._log("info", "Telegram polling остановлен"); return { success: true }; } async _pollLoop() { while (this.polling) { try { - const res = await fetch(this._apiUrl('getUpdates'), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const res = await fetch(this._apiUrl("getUpdates"), { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ offset: this.offset, - timeout: 25, // long-poll: держим соединение до 25с - allowed_updates: ['message', 'callback_query'], + timeout: 25, // long-poll: держим соединение до 25с + allowed_updates: ["message", "callback_query"], }), }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.ok) { - this._log('warn', 'getUpdates error:', (data && data.description) || res.status); + this._log( + "warn", + "getUpdates error:", + (data && data.description) || res.status, + ); await this._sleep(this._pollDelayMs); continue; } const updates = Array.isArray(data.result) ? data.result : []; for (const upd of updates) { - if (typeof upd.update_id === 'number') { + if (typeof upd.update_id === "number") { this.offset = upd.update_id + 1; } // Нажатие inline-кнопки. const cbq = upd.callback_query; if (cbq) { - const cbChat = String(cbq.message && cbq.message.chat && cbq.message.chat.id); + const cbChat = String( + cbq.message && cbq.message.chat && cbq.message.chat.id, + ); if (this.chatId && cbChat !== this.chatId) { - this._log('warn', 'Игнор callback из чужого чата:', cbChat); + this._log("warn", "Игнор callback из чужого чата:", cbChat); continue; } if (this.onCallback) { - try { this.onCallback(cbChat, String(cbq.data || ''), cbq); } catch (e) { - this._log('error', 'onCallback handler error:', e.message); + try { + this.onCallback(cbChat, String(cbq.data || ""), cbq); + } catch (e) { + this._log("error", "onCallback handler error:", e.message); } } continue; @@ -325,12 +377,14 @@ class TelegramBot { const fromChat = String(msg.chat && msg.chat.id); // Принимаем только сообщения от настроенного chat_id (если он задан). if (this.chatId && fromChat !== this.chatId) { - this._log('warn', 'Игнор сообщения из чужого чата:', fromChat); + this._log("warn", "Игнор сообщения из чужого чата:", fromChat); continue; } if (this.onMessage) { - try { this.onMessage(fromChat, msg.text, msg); } catch (e) { - this._log('error', 'onMessage handler error:', e.message); + try { + this.onMessage(fromChat, msg.text, msg); + } catch (e) { + this._log("error", "onMessage handler error:", e.message); } } } @@ -340,7 +394,7 @@ class TelegramBot { } } catch (err) { this._lastError = err.message; - this._log('warn', 'getUpdates exception:', err.message); + this._log("warn", "getUpdates exception:", err.message); await this._sleep(this._pollDelayMs * 2); } } diff --git a/src/main/ipc.js b/src/main/ipc.js index 6782d7c..6211bd5 100644 --- a/src/main/ipc.js +++ b/src/main/ipc.js @@ -221,6 +221,38 @@ async function insertImageToChat(sender, filePath, caption, send) { } } +/** + * Загрузить файл как вложение в поле ввода чата активного окна. + * В отличие от картинок (clipboard + Ctrl+V), произвольные файлы вставляются + * через скрытый на странице чата: формируем File из base64 + * и прокидываем его в input через DataTransfer + событие change. + * @param {Electron.WebContents} sender + * @param {{code:string, fileName:string, size:number}} payload + * @returns {Promise<{success: boolean, fileName?: string, error?: string}>} + */ +async function attachFileToChat(sender, payload) { + try { + if (!sender || sender.isDestroyed()) { + return { success: false, error: "Окно недоступно" }; + } + const code = payload && payload.code; + if (!code || typeof code !== "string") { + return { success: false, error: "Не передано содержимое файла" }; + } + const result = await sender.executeJavaScript(code, true); + if (!result || result.success !== true) { + return { + success: false, + error: (result && result.error) || "Не удалось загрузить вложение", + }; + } + return { success: true, fileName: result.fileName }; + } catch (err) { + console.error("[Cookie Code] attachFileToChat error:", err.message); + return { success: false, error: err.message }; + } +} + /** * Попытаться открыть файл в VS Code (команда code --goto ). * @param {string} filePath — абсолютный путь @@ -601,6 +633,7 @@ function registerIpcHandlers() { requestUserQuestion(event.sender, questions), pasteImage: (filePath, caption, send) => insertImageToChat(event.sender, filePath, caption, send), + attachFile: (payload) => attachFileToChat(event.sender, payload), exitPlanMode: (plan) => requestExitPlanMode(event.sender, plan), }); if (isTaskCanceled(event.sender.id, taskToken)) { @@ -790,6 +823,7 @@ function registerIpcHandlers() { insertImageToChat(event.sender, filePath, caption, send), (plan) => requestExitPlanMode(event.sender, plan), sessionIdOf(event), + (payload) => attachFileToChat(event.sender, payload), ); if (isTaskCanceled(event.sender.id, taskToken)) { return { @@ -894,6 +928,25 @@ function registerIpcHandlers() { } }); + // Индикатор «ИИ печатает…» в Telegram (пока идёт генерация ответа). + ipcMain.handle("telegram-typing-start", async () => { + try { + const bot = require("../../botsrc"); + return bot.startTyping(); + } catch (err) { + return { success: false, error: err.message }; + } + }); + + ipcMain.handle("telegram-typing-stop", async () => { + try { + const bot = require("../../botsrc"); + return bot.stopTyping(); + } catch (err) { + return { success: false, error: err.message }; + } + }); + ipcMain.handle( "telegram-approval-request", async (_event, { requestId, info } = {}) => { diff --git a/src/preload/api.js b/src/preload/api.js index efa49f3..7f823c1 100644 --- a/src/preload/api.js +++ b/src/preload/api.js @@ -44,6 +44,8 @@ let electronAPI = { telegramTest: () => ipcRenderer.invoke("telegram-test"), telegramNotifyAI: (text) => ipcRenderer.invoke("telegram-notify-ai", { text }), + telegramTypingStart: () => ipcRenderer.invoke("telegram-typing-start"), + telegramTypingStop: () => ipcRenderer.invoke("telegram-typing-stop"), telegramApprovalRequest: (requestId, info) => ipcRenderer.invoke("telegram-approval-request", { requestId, info }), telegramApprovalCancel: (requestId) => diff --git a/src/preload/dom/observer.js b/src/preload/dom/observer.js index 025b0c4..18c283f 100644 --- a/src/preload/dom/observer.js +++ b/src/preload/dom/observer.js @@ -3,38 +3,54 @@ * 由原 preload.js 拆分而来,逻辑保持不变。 */ const { - showOverlay, setTaskStatus, showToast, showConfirmDialog, addHistory, flashBadge, truncate, displayCommand, generateId, -} = require('../overlay/ui'); -const { scanForCommands } = require('./detector'); -const { tryParseToolCall } = require('./tool-parser'); -const { getJsCodeBlocksFromMarkdown, looksLikeIncompleteCodeError, FENCE } = require('./js-detector'); -const toolRender = require('./tool-render'); -const toolResultInline = require('./tool-result-inline'); -const responseMeta = require('./response-meta'); - -const { sendToolResultToChat, sendCombinedJsResultsToChat, sendMessageToChat } = require('./chat-input'); -const { isAIResponseComplete } = require('./ai-response'); -const approval = require('./approval'); -const { getProviderByUrl } = require('../../../src/providers'); -const { hasTool, toolNamesList } = require('../tool-names'); -const { t } = require('../i18n/i18n'); -const state = require('./state'); -const { safe } = require('./safe'); + showOverlay, + setTaskStatus, + showToast, + showConfirmDialog, + addHistory, + flashBadge, + truncate, + displayCommand, + generateId, +} = require("../overlay/ui"); +const { scanForCommands } = require("./detector"); +const { tryParseToolCall } = require("./tool-parser"); +const { + getJsCodeBlocksFromMarkdown, + looksLikeIncompleteCodeError, + FENCE, +} = require("./js-detector"); +const toolRender = require("./tool-render"); +const toolResultInline = require("./tool-result-inline"); +const responseMeta = require("./response-meta"); + +const { + sendToolResultToChat, + sendCombinedJsResultsToChat, + sendMessageToChat, +} = require("./chat-input"); +const { isAIResponseComplete } = require("./ai-response"); +const approval = require("./approval"); +const { getProviderByUrl } = require("../../../src/providers"); +const { hasTool, toolNamesList } = require("../tool-names"); +const { t } = require("../i18n/i18n"); +const state = require("./state"); +const { safe } = require("./safe"); /** * 手动解析按钮点击处理 * 用户点击后,仅解析最后一条 AI 回复中的工具调用并执行 */ async function triggerManualParseAttention() { - const btn = document.getElementById('cuckoo-btn-manual-parse'); + const btn = document.getElementById("cuckoo-btn-manual-parse"); if (btn) { - btn.classList.remove('cuckoo-btn-attention'); + btn.classList.remove("cuckoo-btn-attention"); // 强制回流以重新触发动画 void btn.offsetWidth; - btn.classList.add('cuckoo-btn-attention'); + btn.classList.add("cuckoo-btn-attention"); // 动画结束后移除类,避免状态残留 setTimeout(() => { - btn.classList.remove('cuckoo-btn-attention'); + btn.classList.remove("cuckoo-btn-attention"); }, 3500); } } @@ -44,26 +60,26 @@ let isExecuting = false; async function handleManualParse() { if (isExecuting) { - showToast(t('overlay.toast.executing'), 3000); + showToast(t("overlay.toast.executing"), 3000); return; } - const btn = document.getElementById('cuckoo-btn-manual-parse'); + const btn = document.getElementById("cuckoo-btn-manual-parse"); if (btn) { btn.disabled = true; - btn.textContent = t('overlay.btn.manualParse.loading'); + btn.textContent = t("overlay.btn.manualParse.loading"); } try { // 复用自动解析逻辑:仅解析最后一条 AI 回复 processLatestAIResponse(0, true); - showToast(t('overlay.toast.manualParseTriggered'), 3000); + showToast(t("overlay.toast.manualParseTriggered"), 3000); } catch (err) { - console.error('[Cookie Code] 手动解析出错:', err); - showToast(t('overlay.toast.manualParseError', { msg: err.message }), 3000); + console.error("[Cookie Code] 手动解析出错:", err); + showToast(t("overlay.toast.manualParseError", { msg: err.message }), 3000); } finally { if (btn) { btn.disabled = false; - btn.textContent = t('overlay.btn.manualParse'); + btn.textContent = t("overlay.btn.manualParse"); } } } @@ -106,24 +122,36 @@ const STABILITY_POLL_INTERVAL = 500; * 则从第一个 { 开始检查括号配对;配对不完整返回 false(需要重试) */ function isJsonBalanced(str) { - const trimmed = (str || '').trim(); + const trimmed = (str || "").trim(); // 不含 { 或没有工具调用特征 → 不是工具调用,直接通过 - if (!trimmed.includes('{')) return true; - if (!/toolName|"tool"|file_|json复制|```/.test(trimmed) && !trimmed.trimStart().startsWith('{')) { + if (!trimmed.includes("{")) return true; + if ( + !/toolName|"tool"|file_|json复制|```/.test(trimmed) && + !trimmed.trimStart().startsWith("{") + ) { return true; } // 从第一个 { 开始检查括号配对 - const jsonPart = trimmed.substring(trimmed.indexOf('{')); + const jsonPart = trimmed.substring(trimmed.indexOf("{")); let braceCount = 0; let inString = false; let escapeNext = false; for (const char of jsonPart) { - if (escapeNext) { escapeNext = false; continue; } - if (char === '\\') { escapeNext = true; continue; } - if (char === '"') { inString = !inString; continue; } + if (escapeNext) { + escapeNext = false; + continue; + } + if (char === "\\") { + escapeNext = true; + continue; + } + if (char === '"') { + inString = !inString; + continue; + } if (!inString) { - if (char === '{') braceCount++; - else if (char === '}') { + if (char === "{") braceCount++; + else if (char === "}") { braceCount--; if (braceCount < 0) return true; // 多出的 },视为异常但不再等 } @@ -138,12 +166,31 @@ function getCurrentProvider() { return getProviderByUrl(window.location.href); } +/** + * Идёт ли прямо сейчас генерация ответа ИИ. + * Прямой DOM-сигнал: активная (не disabled) круглая primary-кнопка «стоп» + * в composer. В отличие от isAIResponseComplete() не зависит от трактовки + * завершённости и не срабатывает в пустом диалоге. + */ +function isGenerating() { + try { + const stopBtn = document.querySelector( + ".ds-button.ds-button--primary.ds-button--filled.ds-button--circle" + + ".ds-button--m.ds-button--icon-relative-m:not(.ds-button--disabled)", + ); + return !!stopBtn; + } catch (_) { + return false; + } +} + /** * 获取当前平台消息容器元素列表(过滤用户消息) */ function getMessageCandidates() { const provider = getCurrentProvider(); - if (!provider || typeof provider.getMessageCandidates !== 'function') return []; + if (!provider || typeof provider.getMessageCandidates !== "function") + return []; return provider.getMessageCandidates(); } @@ -152,7 +199,8 @@ function getMessageCandidates() { */ function getMessageMarkdown(messageEl) { const provider = getCurrentProvider(); - if (!provider || typeof provider.getMessageMarkdown !== 'function') return messageEl; + if (!provider || typeof provider.getMessageMarkdown !== "function") + return messageEl; return provider.getMessageMarkdown(messageEl); } @@ -177,41 +225,77 @@ async function executeJsBlocksWithRetry(initialBlocks, markdown, force) { } const hasIncompleteFailure = results.some( - item => item && item.result && !item.result.success && looksLikeIncompleteCodeError(item.result.error) + (item) => + item && + item.result && + !item.result.success && + looksLikeIncompleteCodeError(item.result.error), ); if (!hasIncompleteFailure) break; if (attempt < MAX_JS_RETRY) { - console.log('[' + new Date().toISOString() + '] [Cookie Code] ⏳ 代码不完整,等待 1 秒后重新获取并重试(' + (attempt + 1) + '/' + MAX_JS_RETRY + ')...'); + console.log( + "[" + + new Date().toISOString() + + "] [Cookie Code] ⏳ 代码不完整,等待 1 秒后重新获取并重试(" + + (attempt + 1) + + "/" + + MAX_JS_RETRY + + ")...", + ); await sleep(1000); - console.log('[' + new Date().toISOString() + '] [Cookie Code] ⏳ 等待结束,开始第 ' + (attempt + 1) + ' 次重试'); + console.log( + "[" + + new Date().toISOString() + + "] [Cookie Code] ⏳ 等待结束,开始第 " + + (attempt + 1) + + " 次重试", + ); blocks = getJsCodeBlocksFromMarkdown(markdown); } } const stillIncomplete = results.some( - item => item && item.result && !item.result.success && looksLikeIncompleteCodeError(item.result.error) + (item) => + item && + item.result && + !item.result.success && + looksLikeIncompleteCodeError(item.result.error), ); if (stillIncomplete) { - console.log('[' + new Date().toISOString() + '] [Cookie Code] ⚠️ 代码不完整,已重试 ' + MAX_JS_RETRY + ' 次仍失败,将报错回传 AI'); + console.log( + "[" + + new Date().toISOString() + + "] [Cookie Code] ⚠️ 代码不完整,已重试 " + + MAX_JS_RETRY + + " 次仍失败,将报错回传 AI", + ); } if (results.length > 0) { // Инлайн: прикрепляем каждый результат прямо в его карточку вызова, // чтобы пользователь видел результат под кодом, а не отдельным сообщением. for (const item of results) { - try { toolResultInline.markToolBlockResult(item.code, item.result); } catch (_) {} + try { + toolResultInline.markToolBlockResult(item.code, item.result); + } catch (_) {} // Счётчик изменённых строк в шапке инлайн-блока +829 -53 try { - let added = 0, removed = 0; + let added = 0, + removed = 0; const r = item.result; if (r) { if (Array.isArray(r.stats)) { - for (const s of r.stats) { added += Number(s.added)||0; removed += Number(s.removed)||0; } - } else if (r.stats && typeof r.stats.added === 'number') { - added = Number(r.stats.added)||0; removed = Number(r.stats.removed)||0; + for (const s of r.stats) { + added += Number(s.added) || 0; + removed += Number(s.removed) || 0; + } + } else if (r.stats && typeof r.stats.added === "number") { + added = Number(r.stats.added) || 0; + removed = Number(r.stats.removed) || 0; } else if (r.data && r.data.stats) { - added = Number(r.data.stats.added)||0; removed = Number(r.data.stats.removed)||0; + added = Number(r.data.stats.added) || 0; + removed = Number(r.data.stats.removed) || 0; } } // fallback: если stats нет — попробуем оценить из кода (write/edit) чтобы не оставлять пусто @@ -221,13 +305,17 @@ async function executeJsBlocksWithRetry(initialBlocks, markdown, force) { // оставляем пусто — не показываем 0/0 } catch (_) {} } - if (added || removed) toolRender.markToolBlockDiff(item.code, { added, removed }); + if (added || removed) + toolRender.markToolBlockDiff(item.code, { added, removed }); } catch (_) {} } sendCombinedJsResultsToChat(results); // Факт-основанный учёт «затронутых файлов» — только успешные write/edit/delete try { - const msgEl = (markdown && typeof markdown.closest === 'function' ? markdown.closest('.ds-message') : null) || responseMeta.findLatestAIMessage(); + const msgEl = + (markdown && typeof markdown.closest === "function" + ? markdown.closest(".ds-message") + : null) || responseMeta.findLatestAIMessage(); if (msgEl) responseMeta.recordExecutionResults(msgEl, results); } catch (_) {} } @@ -243,7 +331,7 @@ function ensureStabilityTimer() { const now = Date.now(); for (const [msg, rec] of jsStability) { // 节点已被页面卸载:清理 - if (typeof msg.isConnected === 'boolean' && !msg.isConnected) { + if (typeof msg.isConnected === "boolean" && !msg.isConnected) { jsStability.delete(msg); continue; } @@ -262,7 +350,6 @@ function ensureStabilityTimer() { }, STABILITY_POLL_INTERVAL); } - /** * 回复结束后,获取最新一条 AI 回复的内容并解析工具调用 * 改为 async:执行前可能需要等待用户确认(approval gate)。 @@ -276,16 +363,24 @@ function ensureStabilityTimer() { function processLatestAIResponse(retryCount = 0, force = false) { try { const p = processLatestAIResponseInner(retryCount, force); - if (p && typeof p.catch === 'function') { + if (p && typeof p.catch === "function") { p.catch((err) => { const msg = err && err.message ? err.message : String(err); - try { console.warn('[Cookie Code][safe:observer.processLatestAIResponse] ' + msg); } catch (_) {} + try { + console.warn( + "[Cookie Code][safe:observer.processLatestAIResponse] " + msg, + ); + } catch (_) {} }); } return p; } catch (err) { const msg = err && err.message ? err.message : String(err); - try { console.warn('[Cookie Code][safe:observer.processLatestAIResponse] ' + msg); } catch (_) {} + try { + console.warn( + "[Cookie Code][safe:observer.processLatestAIResponse] " + msg, + ); + } catch (_) {} return undefined; } } @@ -302,7 +397,7 @@ async function processLatestAIResponseInner(retryCount = 0, force = false) { for (let i = messages.length - 1; i >= 0; i--) { const candidate = messages[i]; const md = getMessageMarkdown(candidate); - const hasContent = md && (md.textContent || '').trim().length > 0; + const hasContent = md && (md.textContent || "").trim().length > 0; if (hasContent) { lastMessage = candidate; markdown = md; @@ -310,7 +405,7 @@ async function processLatestAIResponseInner(retryCount = 0, force = false) { } } if (!lastMessage || !markdown) { - console.log('[Cookie Code] 未找到有内容的 AI 回复'); + console.log("[Cookie Code] 未找到有内容的 AI 回复"); return; } @@ -320,9 +415,13 @@ async function processLatestAIResponseInner(retryCount = 0, force = false) { // 跳过用户消息(其中包含系统提示词里的示例代码块,不应被执行) const providerForUser = getCurrentProvider(); - if (providerForUser && typeof providerForUser.isUserMessage === 'function' && providerForUser.isUserMessage(lastMessage)) { + if ( + providerForUser && + typeof providerForUser.isUserMessage === "function" && + providerForUser.isUserMessage(lastMessage) + ) { processedMessages.add(lastMessage); - console.log('[Cookie Code] ⏭ 跳过用户消息(包含系统提示词示例)'); + console.log("[Cookie Code] ⏭ 跳过用户消息(包含系统提示词示例)"); return; } @@ -340,11 +439,18 @@ async function processLatestAIResponseInner(retryCount = 0, force = false) { // 优先检测 JS 工具代码块(cuckoo 代码块 / 调用工具函数的 js 代码块) const jsBlocks = getJsCodeBlocksFromMarkdown(markdown); - console.log('[DEBUG][processLatest] lastMessage=' + (lastMessage.className || lastMessage.tagName) + - ' markdown=' + (markdown.className || markdown.tagName) + - ' jsBlocks=' + jsBlocks.length + - ' force=' + force + - ' retryCount=' + retryCount); + console.log( + "[DEBUG][processLatest] lastMessage=" + + (lastMessage.className || lastMessage.tagName) + + " markdown=" + + (markdown.className || markdown.tagName) + + " jsBlocks=" + + jsBlocks.length + + " force=" + + force + + " retryCount=" + + retryCount, + ); if (jsBlocks.length > 0) { // 稳定性双通道校验:流式渲染期间代码块只渲染了一半(曾导致 "const content" // 这样的残缺代码被执行 → SyntaxError)。mutation 驱动 + interval 兜底, @@ -352,33 +458,43 @@ async function processLatestAIResponseInner(retryCount = 0, force = false) { if (force) { // 手动解析:跳过稳定性校验,直接执行(标记已处理,避免同节点重复自动执行) processedMessages.add(lastMessage); - console.log('[Cookie Code] 手动解析模式,跳过稳定性校验'); + console.log("[Cookie Code] 手动解析模式,跳过稳定性校验"); executeJsBlocksWithRetry(jsBlocks, markdown, true); return; } - const snapshot = markdown.textContent || ''; - const blocksSig = jsBlocks.map((b) => b.length).join(','); + const snapshot = markdown.textContent || ""; + const blocksSig = jsBlocks.map((b) => b.length).join(","); const now = Date.now(); const rec = jsStability.get(lastMessage); if (!rec || rec.snapshot !== snapshot || rec.blocksSig !== blocksSig) { // 内容仍在变化:记录快照,等待下一次 mutation / interval 复查 jsStability.set(lastMessage, { snapshot, blocksSig, lastChange: now }); ensureStabilityTimer(); - console.log('[Cookie Code] ⏳ 检测到 JS 工具代码块,流式渲染中,等待稳定...'); + console.log( + "[Cookie Code] ⏳ 检测到 JS 工具代码块,流式渲染中,等待稳定...", + ); return; // 不标记 processed,稳定后执行 } // 内容一致:需稳定满窗口确认 if (now - rec.lastChange < JS_STABILITY_WINDOW) { - console.log('[Cookie Code] ⏳ JS 代码块稳定中(等待 ' + JS_STABILITY_WINDOW + 'ms 确认)...'); + console.log( + "[Cookie Code] ⏳ JS 代码块稳定中(等待 " + + JS_STABILITY_WINDOW + + "ms 确认)...", + ); return; } // 稳定满窗口 → 执行 jsStability.delete(lastMessage); processedMessages.add(lastMessage); - console.log('[Cookie Code] ✅ 代码块稳定,检测到 JS 工具代码块(' + jsBlocks.length + ' 个),开始执行'); + console.log( + "[Cookie Code] ✅ 代码块稳定,检测到 JS 工具代码块(" + + jsBlocks.length + + " 个),开始执行", + ); // 正确使用 cuckoo 代码块,重置 XML 提示计数 xmlHintCount = 0; executeJsBlocksWithRetry(jsBlocks, markdown, false); @@ -390,74 +506,119 @@ async function processLatestAIResponseInner(retryCount = 0, force = false) { // 导致同一条消息后续渲染出的完整代码块被永久跳过(智谱等 SPA 回复中途 // isResponseComplete 即可能返回 true)。与 JS 块共用稳定性通道。 if (!force) { - const snapshot = markdown.textContent || ''; + const snapshot = markdown.textContent || ""; const now = Date.now(); const rec = jsStability.get(lastMessage); if (!rec || rec.snapshot !== snapshot) { - jsStability.set(lastMessage, { snapshot, blocksSig: 'text', lastChange: now }); + jsStability.set(lastMessage, { + snapshot, + blocksSig: "text", + lastChange: now, + }); ensureStabilityTimer(); - console.log('[Cookie Code] ⏳ 文本内容渲染中,等待稳定(防流式中途漏检)...'); + console.log( + "[Cookie Code] ⏳ 文本内容渲染中,等待稳定(防流式中途漏检)...", + ); return; } if (now - rec.lastChange < JS_STABILITY_WINDOW) { - console.log('[Cookie Code] ⏳ 文本内容稳定中(等待 ' + JS_STABILITY_WINDOW + 'ms 确认)...'); + console.log( + "[Cookie Code] ⏳ 文本内容稳定中(等待 " + + JS_STABILITY_WINDOW + + "ms 确认)...", + ); return; } jsStability.delete(lastMessage); } // 提取文本:优先从 pre code 提取(代码块内容天然不含 json/复制/下载等按钮文字) - let text = ''; - const codeEl = markdown.querySelector('pre code'); + let text = ""; + const codeEl = markdown.querySelector("pre code"); if (codeEl) { - text = (codeEl.textContent || codeEl.innerText || '').trim(); - console.log('[Cookie Code] 提取方式: pre code 元素'); + text = (codeEl.textContent || codeEl.innerText || "").trim(); + console.log("[Cookie Code] 提取方式: pre code 元素"); } else { // 无代码块:克隆节点并剔除可能的工具栏元素 const clone = markdown.cloneNode(true); - clone.querySelectorAll('button, [class*="toolbar"], [class*="copy"], [class*="download"], [class*="code-block-header"], [class*="lang"], [class*="header"]').forEach(el => el.remove()); - text = (clone.textContent || clone.innerText || '').trim(); - console.log('[Cookie Code] 提取方式: 克隆节点(剔除工具栏)'); + clone + .querySelectorAll( + 'button, [class*="toolbar"], [class*="copy"], [class*="download"], [class*="code-block-header"], [class*="lang"], [class*="header"]', + ) + .forEach((el) => el.remove()); + text = (clone.textContent || clone.innerText || "").trim(); + console.log("[Cookie Code] 提取方式: 克隆节点(剔除工具栏)"); } if (!text) { - console.log('[DEBUG][processLatest] 提取文本为空'); + console.log("[DEBUG][processLatest] 提取文本为空"); return; } - console.log('[DEBUG][processLatest] text长度=' + text.length + ' 前60字符=' + JSON.stringify(text.slice(0, 60))); + console.log( + "[DEBUG][processLatest] text长度=" + + text.length + + " 前60字符=" + + JSON.stringify(text.slice(0, 60)), + ); console.log(text); // Декорируем cuckoo-блоки в чате (раскрывающиеся tool-блоки) — // только при включённой кастомизации; парсинг ниже работает всегда. if (state.customizationEnabled !== false) { - try { toolRender.decorate(markdown); } catch (e) { /* не критично */ } + try { + toolRender.decorate(markdown); + } catch (e) { + /* не критично */ + } } // 是否为疑似工具内容(用于控制详细日志与提示文案) - const looksToolish = text.includes(FENCE) || - /toolName|"tool"|file_|await\s+(?:read|write|edit|glob|grep|bash|pwsh|todoWrite|deleteFile|webFetch|cityTime|openBrowserWindow|injectJS|readFile|writeFile|editFile)\s*\(/.test(text); + const looksToolish = + text.includes(FENCE) || + /toolName|"tool"|file_|await\s+(?:read|write|edit|glob|grep|bash|pwsh|todoWrite|deleteFile|webFetch|cityTime|openBrowserWindow|injectJS|readFile|writeFile|editFile)\s*\(/.test( + text, + ); // 长度必打;原文/转义仅在疑似工具内容时打印(普通聊天回复不再刷屏) - console.log('[Cookie Code] 回复文本长度: ' + text.length + (looksToolish ? '(疑似工具内容)' : '(普通文本)')); + console.log( + "[Cookie Code] 回复文本长度: " + + text.length + + (looksToolish ? "(疑似工具内容)" : "(普通文本)"), + ); if (looksToolish) { - console.log('[Cookie Code] 回复完整内容(原文):'); + console.log("[Cookie Code] 回复完整内容(原文):"); console.log(text); - console.log('[Cookie Code] 回复完整内容(转义显示):'); + console.log("[Cookie Code] 回复完整内容(转义显示):"); console.log(JSON.stringify(text)); } // 内容不完整(疑似流式输出未真正结束):延迟重试,避免处理截断的 JSON if (!force && !isJsonBalanced(text)) { if (retryCount < MAX_RETRY_COUNT) { - console.log('[Cookie Code] ⏳ JSON 不完整(疑似流式未结束),' + (retryCount + 1) + '/' + MAX_RETRY_COUNT + ' 次延迟重试, 当前长度=' + text.length + '...'); + console.log( + "[Cookie Code] ⏳ JSON 不完整(疑似流式未结束)," + + (retryCount + 1) + + "/" + + MAX_RETRY_COUNT + + " 次延迟重试, 当前长度=" + + text.length + + "...", + ); setTimeout(() => processLatestAIResponse(retryCount + 1), RETRY_INTERVAL); return; // 不标记 processed,允许重试 } - console.log('[Cookie Code] ⚠️ JSON 持续不完整(20次重试仍截断),放弃本次处理,当前长度=' + text.length); + console.log( + "[Cookie Code] ⚠️ JSON 持续不完整(20次重试仍截断),放弃本次处理,当前长度=" + + text.length, + ); // 回传 AI,让它重新完整输出 sendToolResultToChat( - { toolName: '未知', callId: 'incomplete' }, - { success: false, error: '收到不完整的工具调用 JSON(内容被截断),请重新完整输出工具调用。' } + { toolName: "未知", callId: "incomplete" }, + { + success: false, + error: + "收到不完整的工具调用 JSON(内容被截断),请重新完整输出工具调用。", + }, ); } @@ -470,31 +631,39 @@ async function processLatestAIResponseInner(retryCount = 0, force = false) { // 验证 toolName 是否在工具库中 const available = hasTool(toolCall.toolName); if (!available) { - console.log('[Cookie Code] ⚠️ 工具不存在: ' + toolCall.toolName + ', 可用工具: ' + toolNamesList()); - // 回传 AI,告知工具不存在 - sendToolResultToChat( - toolCall, - { success: false, error: '工具 ' + toolCall.toolName + ' 不存在,可用工具: ' + toolNamesList() } + console.log( + "[Cookie Code] ⚠️ 工具不存在: " + + toolCall.toolName + + ", 可用工具: " + + toolNamesList(), ); + // 回传 AI,告知工具不存在 + sendToolResultToChat(toolCall, { + success: false, + error: + "工具 " + toolCall.toolName + " 不存在,可用工具: " + toolNamesList(), + }); return; } - console.log('[Cookie Code] ✅ 工具存在: ' + toolCall.toolName + ', 开始执行'); + console.log( + "[Cookie Code] ✅ 工具存在: " + toolCall.toolName + ", 开始执行", + ); notifyToolCallDetected(toolCall); // ===== Approval gate:按 toolApprovalMode 在执行前请求用户确认 ===== let verdict = { approved: true }; try { verdict = await approval.requestApprovalIfNeeded({ - kind: 'tool', + kind: "tool", toolName: toolCall.toolName, params: toolCall.params, }); } catch (err) { - console.error('[Cookie Code] approval gate error:', err.message); + console.error("[Cookie Code] approval gate error:", err.message); verdict = { approved: false }; } if (!verdict.approved) { - console.log('[Cookie Code] ⛔ 工具被用户拒绝: ' + toolCall.toolName); + console.log("[Cookie Code] ⛔ 工具被用户拒绝: " + toolCall.toolName); // 回传 AI(经隐身通道),告知调用被拒绝,不要盲目重试 sendToolResultToChat(toolCall, { success: false, @@ -508,9 +677,22 @@ async function processLatestAIResponseInner(retryCount = 0, force = false) { } else { // JSON 工具调用未解析到,再检测 XML 格式的工具调用 // 诊断:打印 XML 检测相关状态(text 和 innerHTML) - console.log('[Cookie Code] [XML诊断] text长度=' + text.length + ', 开头100字符=' + JSON.stringify(text.slice(0, 100))); - console.log('[Cookie Code] [XML诊断] markdown.innerHTML长度=' + (markdown.innerHTML || '').length + ', 开头200字符=' + JSON.stringify((markdown.innerHTML || '').slice(0, 200))); - console.log('[Cookie Code] [XML诊断] 是否有 pre code 元素=' + !!markdown.querySelector('pre code')); + console.log( + "[Cookie Code] [XML诊断] text长度=" + + text.length + + ", 开头100字符=" + + JSON.stringify(text.slice(0, 100)), + ); + console.log( + "[Cookie Code] [XML诊断] markdown.innerHTML长度=" + + (markdown.innerHTML || "").length + + ", 开头200字符=" + + JSON.stringify((markdown.innerHTML || "").slice(0, 200)), + ); + console.log( + "[Cookie Code] [XML诊断] 是否有 pre code 元素=" + + !!markdown.querySelector("pre code"), + ); // 精准判断: // 1. <||DSML|| 开头直接触发(自定义标签前缀,如 <||DSML||tool_calls>、<||DSML||invoke>) // 2. = XML_HINT_MAX) { // 已连续提示多次,AI 仍用 XML 格式,熔断停止发送,避免无限循环 - console.log('[Cookie Code] ⚠️ 已连续提示 ' + xmlHintCount + ' 次 XML 格式,停止发送提示语'); + console.log( + "[Cookie Code] ⚠️ 已连续提示 " + + xmlHintCount + + " 次 XML 格式,停止发送提示语", + ); return; } xmlHintCount++; - console.log('[Cookie Code] ⚠️ 检测到 XML 格式工具调用(第 ' + xmlHintCount + ' 次提示),提示 AI 改用 cuckoo 代码块'); + console.log( + "[Cookie Code] ⚠️ 检测到 XML 格式工具调用(第 " + + xmlHintCount + + " 次提示),提示 AI 改用 cuckoo 代码块", + ); const BT = String.fromCharCode(96); sendMessageToChat( - '请使用' + BT + BT + BT + 'cuckoo' + BT + BT + BT + ' 代码块进行工具调用,不要使用 XML invoke 格式。', - 'XML工具调用提示' + "请使用" + + BT + + BT + + BT + + "cuckoo" + + BT + + BT + + BT + + " 代码块进行工具调用,不要使用 XML invoke 格式。", + "XML工具调用提示", ); return; } if (looksToolish) { // 疑似工具内容但 JS 块检测与 JSON 解析都没命中 → 打印诊断,帮助定位 - console.log('[Cookie Code] ⚠️ 回复疑似工具调用但未被识别(JS 代码块未匹配 / JSON 解析失败)'); - const pres = markdown.querySelectorAll('pre'); + console.log( + "[Cookie Code] ⚠️ 回复疑似工具调用但未被识别(JS 代码块未匹配 / JSON 解析失败)", + ); + const pres = markdown.querySelectorAll("pre"); if (pres.length > 0) { for (const p of pres) { const providerForLang = getCurrentProvider(); - const lang = (providerForLang && typeof providerForLang.getCodeBlockLanguage === 'function') - ? providerForLang.getCodeBlockLanguage(p) - : ''; - console.log('[Cookie Code] [诊断] 代码块 language=' + (lang || '(无)') + ', 内容前80字符=' + ((p.textContent || '').trim().slice(0, 80))); + const lang = + providerForLang && + typeof providerForLang.getCodeBlockLanguage === "function" + ? providerForLang.getCodeBlockLanguage(p) + : ""; + console.log( + "[Cookie Code] [诊断] 代码块 language=" + + (lang || "(无)") + + ", 内容前80字符=" + + (p.textContent || "").trim().slice(0, 80), + ); } } else { - console.log('[Cookie Code] [诊断] 消息中没有任何 pre 代码块'); + console.log("[Cookie Code] [诊断] 消息中没有任何 pre 代码块"); } } else { - console.log('[Cookie Code] ℹ️ 正常文本回复,未检测到工具调用(无需处理)'); + console.log( + "[Cookie Code] ℹ️ 正常文本回复,未检测到工具调用(无需处理)", + ); // 防重复:同一文本不重复通知(完成检测轮询每 2s 触发一次,避免刷屏) if (lastNotifiedText !== text) { lastNotifiedText = text; @@ -566,9 +775,9 @@ async function processLatestAIResponseInner(retryCount = 0, force = false) { let isProcessingResponse = false; let lastObserverRun = 0; let completionPollTimer = null; -let lastNotifiedText = ''; +let lastNotifiedText = ""; // Последний текст, отправленный в Telegram (антидубль). -let lastAiTgText = ''; +let lastAiTgText = ""; /** * Убрать из текста markdown code-блоки, оставив человеческий текст. @@ -579,51 +788,84 @@ let lastAiTgText = ''; * code-блоки и тулбары, вернуть очищенный textContent. */ function extractHumanTextFromNode(node) { - if (!node) return ''; + if (!node) return ""; const clone = node.cloneNode(true); - clone.querySelectorAll('.md-code-block, .cuckoo-tool-block, .cuckoo-tool-header, .cuckoo-tool-label, .cuckoo-tool-file, .cuckoo-tool-sep, .cuckoo-tool-icon, .cuckoo-tool-chevron, pre, button, [class*="toolbar"], [class*="copy"], [class*="download"], [class*="code-block"], [class*="lang"]').forEach(el => el.remove()); + clone + .querySelectorAll( + '.md-code-block, .cuckoo-tool-block, .cuckoo-tool-header, .cuckoo-tool-label, .cuckoo-tool-file, .cuckoo-tool-sep, .cuckoo-tool-icon, .cuckoo-tool-chevron, pre, button, [class*="toolbar"], [class*="copy"], [class*="download"], [class*="code-block"], [class*="lang"]', + ) + .forEach((el) => el.remove()); // Обходим DOM и расставляем переносы на границах блочных элементов — // textContent их склеивает (абзацы/пункты списка/заголовки). - const BLOCK = { P: 1, DIV: 1, LI: 1, UL: 1, OL: 1, BR: 1, TR: 1, H1: 1, H2: 1, H3: 1, H4: 1, H5: 1, H6: 1, BLOCKQUOTE: 1, TABLE: 1, SECTION: 1, ARTICLE: 1 }; + const BLOCK = { + P: 1, + DIV: 1, + LI: 1, + UL: 1, + OL: 1, + BR: 1, + TR: 1, + H1: 1, + H2: 1, + H3: 1, + H4: 1, + H5: 1, + H6: 1, + BLOCKQUOTE: 1, + TABLE: 1, + SECTION: 1, + ARTICLE: 1, + }; const out = []; const walk = (el) => { - const tag = el.nodeName ? el.nodeName.toUpperCase() : ''; - if (tag === 'BR') { out.push('\n'); return; } + const tag = el.nodeName ? el.nodeName.toUpperCase() : ""; + if (tag === "BR") { + out.push("\n"); + return; + } for (let i = 0; i < el.childNodes.length; i++) { const child = el.childNodes[i]; if (child.nodeType === 3) { out.push(child.nodeValue); } else if (child.nodeType === 1) { const isBlock = !!BLOCK[child.nodeName.toUpperCase()]; - if (isBlock) out.push('\n'); + if (isBlock) out.push("\n"); walk(child); - if (isBlock) out.push('\n'); + if (isBlock) out.push("\n"); } } }; walk(clone); - let t = out.join('').replace(/[ \t]+/g, ' ').replace(/ *\n */g, '\n').replace(/\n{3,}/g, '\n\n').trim(); + let t = out + .join("") + .replace(/[ \t]+/g, " ") + .replace(/ *\n */g, "\n") + .replace(/\n{3,}/g, "\n\n") + .trim(); return t; } function extractHumanText(raw) { - if (!raw) return ''; + if (!raw) return ""; const BT = String.fromCharCode(96); const fence = BT + BT + BT; let t = String(raw); - const re = new RegExp(fence + '[\\s\\S]*?' + fence, 'g'); - t = t.replace(re, ' '); - t = t.replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim(); + const re = new RegExp(fence + "[\\s\\S]*?" + fence, "g"); + t = t.replace(re, " "); + t = t + .replace(/[ \t]+/g, " ") + .replace(/\n{3,}/g, "\n\n") + .trim(); return t; } function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } function startObserver() { if (state.customizationEnabled !== false) { - safe('observer.startObserver.toolRender', () => toolRender.startWatch()); + safe("observer.startObserver.toolRender", () => toolRender.startWatch()); } - safe('observer.startObserver.responseMeta', () => responseMeta.startWatch()); + safe("observer.startObserver.responseMeta", () => responseMeta.startWatch()); const observer = new MutationObserver((mutations) => { // 节流:避免页面高频 DOM 变化导致日志与检测刷屏。 @@ -637,7 +879,7 @@ function startObserver() { // Это дорогая операция, а нам нужно лишь понять «что-то изменилось». let hasNewContent = false; for (const mutation of mutations) { - if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { + if (mutation.type === "childList" && mutation.addedNodes.length > 0) { // scanForCommands вызываем только для «осмысленных» узлов (не text). const meaningful = []; for (let i = 0; i < mutation.addedNodes.length; i++) { @@ -647,11 +889,18 @@ function startObserver() { if (meaningful.length > 0) { const commands = scanForCommands(meaningful); for (const cmd of commands) { - displayCommand({ command: cmd, timestamp: Date.now(), id: generateId() }); + displayCommand({ + command: cmd, + timestamp: Date.now(), + id: generateId(), + }); } hasNewContent = true; } - } else if (mutation.type === 'characterData' || mutation.type === 'attributes') { + } else if ( + mutation.type === "characterData" || + mutation.type === "attributes" + ) { hasNewContent = true; } } @@ -662,7 +911,10 @@ function startObserver() { // мгновенно, как только новый AI-элемент появился в DOM. try { const metaCandidates = getMessageCandidates(); - const latestForMeta = metaCandidates.length > 0 ? metaCandidates[metaCandidates.length - 1] : null; + const latestForMeta = + metaCandidates.length > 0 + ? metaCandidates[metaCandidates.length - 1] + : null; if (latestForMeta) responseMeta.startTimer(latestForMeta); } catch (_) {} @@ -670,7 +922,8 @@ function startObserver() { if (hasNewContent && !isProcessingResponse) { // 若最后一条 AI 消息已处理过,则跳过,避免反复打印和等待 const candidates = getMessageCandidates(); - const lastMsg = candidates.length > 0 ? candidates[candidates.length - 1] : null; + const lastMsg = + candidates.length > 0 ? candidates[candidates.length - 1] : null; if (lastMsg && processedMessages.has(lastMsg)) { return; } @@ -678,8 +931,20 @@ function startObserver() { isProcessingResponse = true; (async () => { try { + // Индикатор «печатает…» в TG — только пока реально идёт генерация. + if (isGenerating()) { + try { + window.electronAPI.telegramTypingStart(); + } catch (_) {} + } else { + try { + window.electronAPI.telegramTypingStop(); + } catch (_) {} + } if (await isAIResponseComplete()) { - try { responseMeta.finishTimer(responseMeta.findLatestAIMessage()); } catch (_) {} + try { + responseMeta.finishTimer(responseMeta.findLatestAIMessage()); + } catch (_) {} processLatestAIResponse(); } } finally { @@ -703,40 +968,56 @@ function startObserver() { if (isProcessingResponse) return; (async () => { try { + // Страховка: генерация закончилась, но mutation-сигнал потерялся — + // гасим «печатает…» в TG. + if (!isGenerating()) { + try { + window.electronAPI.telegramTypingStop(); + } catch (_) {} + } if (await isAIResponseComplete()) { - try { responseMeta.finishTimer(responseMeta.findLatestAIMessage()); } catch (_) {} + try { + responseMeta.finishTimer(responseMeta.findLatestAIMessage()); + } catch (_) {} processLatestAIResponse(); } - } catch (_) { /* 轮询失败静默,等待下一轮 */ } + } catch (_) { + /* 轮询失败静默,等待下一轮 */ + } })(); }, 2000); } } - /** * 通知用户检测到工具调用(闪烁状态徽章 + 展开覆盖层) */ function notifyToolCallDetected(toolCall) { // 方向 C:不强制弹面板,只更新预览和徽章 // 更新预览区域显示检测到的工具调用 - const preview = document.getElementById('cuckoo-cmd-preview'); + const preview = document.getElementById("cuckoo-cmd-preview"); if (preview) { - preview.textContent = t('overlay.preview.toolPrefix', { name: toolCall.toolName }) + String.fromCharCode(10) + t('overlay.preview.params', { json: JSON.stringify(toolCall.params, null, 2) }); + preview.textContent = + t("overlay.preview.toolPrefix", { name: toolCall.toolName }) + + String.fromCharCode(10) + + t("overlay.preview.params", { + json: JSON.stringify(toolCall.params, null, 2), + }); } // 闪烁状态徽章 - flashBadge(t('overlay.badge.toolDetected')); + flashBadge(t("overlay.badge.toolDetected")); } /** * 通知用户检测到 JS 工具脚本(更新预览 + 闪烁徽章) */ function notifyJsScriptDetected(code) { // 方向 C:不强制弹面板 - const preview = document.getElementById('cuckoo-cmd-preview'); + const preview = document.getElementById("cuckoo-cmd-preview"); if (preview) { - preview.textContent = t('overlay.preview.jsPrefix') + String.fromCharCode(10) + code; + preview.textContent = + t("overlay.preview.jsPrefix") + String.fromCharCode(10) + code; } - flashBadge(t('overlay.badge.jsDetected')); + flashBadge(t("overlay.badge.jsDetected")); } /** * 执行检测到的 JS 工具脚本(带双通道去重) @@ -745,90 +1026,127 @@ async function handleJsToolScript(code) { // ===== Approval gate:JS 块同样按 toolApprovalMode 请求用户确认 ===== let verdict = { approved: true }; try { - verdict = await approval.requestApprovalIfNeeded({ kind: 'js', code }); + verdict = await approval.requestApprovalIfNeeded({ kind: "js", code }); } catch (err) { - console.error('[Cookie Code] approval gate error:', err.message); + console.error("[Cookie Code] approval gate error:", err.message); verdict = { approved: false }; } if (!verdict.approved) { - console.log('[Cookie Code] ⛔ JS 脚本被用户拒绝'); + console.log("[Cookie Code] ⛔ JS 脚本被用户拒绝"); // 作为失败结果合并回传 AI(denied=true → 不提示“修正后重试”) - return { code, result: { success: false, denied: true, error: approval.DENIED_JS_ERROR } }; + return { + code, + result: { success: false, denied: true, error: approval.DENIED_JS_ERROR }, + }; } // 方向 C:不强制弹面板 isExecuting = true; notifyJsScriptDetected(code); setTaskStatus(true); - showToast(t('overlay.toast.execStarted')); + showToast(t("overlay.toast.execStarted")); - const callId = 'js_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6); - console.log('[Cookie Code] [诊断] 即将执行的代码(JSON转义): ' + JSON.stringify(code)); + const callId = + "js_" + Date.now() + "_" + Math.random().toString(36).substr(2, 6); + console.log( + "[Cookie Code] [诊断] 即将执行的代码(JSON转义): " + JSON.stringify(code), + ); try { const result = await window.electronAPI.executeJs(code, callId); - const resultSection = document.getElementById('cuckoo-result-section'); - const resultStatus = document.getElementById('cuckoo-result-status'); - const resultOutput = document.getElementById('cuckoo-result-output'); - if (resultSection) resultSection.classList.remove('cuckoo-hidden'); + const resultSection = document.getElementById("cuckoo-result-section"); + const resultStatus = document.getElementById("cuckoo-result-status"); + const resultOutput = document.getElementById("cuckoo-result-output"); + if (resultSection) resultSection.classList.remove("cuckoo-hidden"); // Определяем «фактическую ошибку»: result.success=false ИЛИ в выводе ненулевой exit code. - const outText = String(result.output || ''); + const outText = String(result.output || ""); const exitMatch = outText.match(/\[exit code:\s*(-?\d+)\]/); - const hasBadExit = !!(exitMatch && exitMatch[1] !== '0'); + const hasBadExit = !!(exitMatch && exitMatch[1] !== "0"); if (result.success) { if (resultStatus) { resultStatus.textContent = hasBadExit - ? t('overlay.status.jsExit', { code: exitMatch[1] }) - : t('overlay.status.jsOk'); - resultStatus.className = hasBadExit ? 'cuckoo-result-status error' : 'cuckoo-result-status success'; + ? t("overlay.status.jsExit", { code: exitMatch[1] }) + : t("overlay.status.jsOk"); + resultStatus.className = hasBadExit + ? "cuckoo-result-status error" + : "cuckoo-result-status success"; } if (resultOutput) { - resultOutput.textContent = result.output || t('overlay.output.scriptDone'); + resultOutput.textContent = + result.output || t("overlay.output.scriptDone"); } if (hasBadExit) { try { - toolRender.markToolBlockError(code, 'Команда завершилась с кодом ' + exitMatch[1] + ': ' + outText.slice(0, 300)); + toolRender.markToolBlockError( + code, + "Команда завершилась с кодом " + + exitMatch[1] + + ": " + + outText.slice(0, 300), + ); } catch (_) {} } } else { if (resultStatus) { - resultStatus.textContent = t('overlay.status.jsFailed'); - resultStatus.className = 'cuckoo-result-status error'; + resultStatus.textContent = t("overlay.status.jsFailed"); + resultStatus.className = "cuckoo-result-status error"; } if (resultOutput) { - resultOutput.textContent = result.error || t('overlay.output.unknownError'); + resultOutput.textContent = + result.error || t("overlay.output.unknownError"); } // Помечаем tool-блок в чате как ошибочный - try { toolRender.markToolBlockError(code, result.error || t('overlay.output.execFailed')); } catch (_) {} + try { + toolRender.markToolBlockError( + code, + result.error || t("overlay.output.execFailed"), + ); + } catch (_) {} } addHistory({ id: callId, - command: '[JS] ' + truncate((code.split(String.fromCharCode(10))[0] || code), 60), + command: + "[JS] " + truncate(code.split(String.fromCharCode(10))[0] || code, 60), success: result.success && !hasBadExit, - output: result.success ? (result.output || '') : (result.error || t('overlay.output.unknownError')), + output: result.success + ? result.output || "" + : result.error || t("overlay.output.unknownError"), timestamp: Date.now(), }); // 返回执行结果,由调用方统一合并回传 return { code, result }; } catch (err) { - console.error('[Cookie Code] JS 工具脚本执行异常:', err); - const resultSection = document.getElementById('cuckoo-result-section'); - const resultStatus = document.getElementById('cuckoo-result-status'); - const resultOutput = document.getElementById('cuckoo-result-output'); - if (resultSection) resultSection.classList.remove('cuckoo-hidden'); + console.error("[Cookie Code] JS 工具脚本执行异常:", err); + const resultSection = document.getElementById("cuckoo-result-section"); + const resultStatus = document.getElementById("cuckoo-result-status"); + const resultOutput = document.getElementById("cuckoo-result-output"); + if (resultSection) resultSection.classList.remove("cuckoo-hidden"); if (resultStatus) { - resultStatus.textContent = t('overlay.status.sysError'); - resultStatus.className = 'cuckoo-result-status error'; + resultStatus.textContent = t("overlay.status.sysError"); + resultStatus.className = "cuckoo-result-status error"; } if (resultOutput) { resultOutput.textContent = err.message || String(err); } - try { toolRender.markToolBlockError(code, 'Системная ошибка: ' + (err.message || String(err))); } catch (_) {} - return { code, result: { success: false, error: t('overlay.output.systemException', { msg: err.message || String(err) }) } }; + try { + toolRender.markToolBlockError( + code, + "Системная ошибка: " + (err.message || String(err)), + ); + } catch (_) {} + return { + code, + result: { + success: false, + error: t("overlay.output.systemException", { + msg: err.message || String(err), + }), + }, + }; } finally { isExecuting = false; setTaskStatus(false); @@ -844,66 +1162,83 @@ async function handleToolCall(toolCall) { // 方向 C:不强制弹面板 isExecuting = true; setTaskStatus(true); - showToast(t('overlay.toast.execStarted')); + showToast(t("overlay.toast.execStarted")); try { - const result = await window.electronAPI.executeTool(toolName, params, callId); + const result = await window.electronAPI.executeTool( + toolName, + params, + callId, + ); // 显示执行结果 - const resultSection = document.getElementById('cuckoo-result-section'); - const resultStatus = document.getElementById('cuckoo-result-status'); - const resultOutput = document.getElementById('cuckoo-result-output'); + const resultSection = document.getElementById("cuckoo-result-section"); + const resultStatus = document.getElementById("cuckoo-result-status"); + const resultOutput = document.getElementById("cuckoo-result-output"); - if (resultSection) resultSection.classList.remove('cuckoo-hidden'); + if (resultSection) resultSection.classList.remove("cuckoo-hidden"); if (result.success) { if (resultStatus) { - resultStatus.textContent = t('overlay.status.toolOk', { name: toolName }); - resultStatus.className = 'cuckoo-result-status success'; + resultStatus.textContent = t("overlay.status.toolOk", { + name: toolName, + }); + resultStatus.className = "cuckoo-result-status success"; } if (resultOutput) { resultOutput.textContent = JSON.stringify(result.data, null, 2); } } else { if (resultStatus) { - resultStatus.textContent = t('overlay.status.toolFailed', { name: toolName }); - resultStatus.className = 'cuckoo-result-status error'; + resultStatus.textContent = t("overlay.status.toolFailed", { + name: toolName, + }); + resultStatus.className = "cuckoo-result-status error"; } if (resultOutput) { - resultOutput.textContent = result.error || t('overlay.output.unknownError'); + resultOutput.textContent = + result.error || t("overlay.output.unknownError"); } } // 添加到历史 addHistory({ id: callId, - command: t('overlay.preview.toolPrefix', { name: toolName }), + command: t("overlay.preview.toolPrefix", { name: toolName }), success: result.success, - output: result.success ? JSON.stringify(result.data, null, 2) : (result.error || t('overlay.output.unknownError')), + output: result.success + ? JSON.stringify(result.data, null, 2) + : result.error || t("overlay.output.unknownError"), timestamp: Date.now(), }); // Факт-основанный учёт файлов — только успешные file-операции try { const msgEl = responseMeta.findLatestAIMessage(); - if (msgEl) responseMeta.recordSingleToolCall(msgEl, toolName, params, result); + if (msgEl) + responseMeta.recordSingleToolCall(msgEl, toolName, params, result); } catch (_) {} // 将执行结果发送回聊天,让 AI 看到结果并继续工作 sendToolResultToChat(toolCall, result); } catch (err) { - console.error('[Cookie Code] 工具执行异常:', err); - const resultSection = document.getElementById('cuckoo-result-section'); - const resultStatus = document.getElementById('cuckoo-result-status'); - const resultOutput = document.getElementById('cuckoo-result-output'); - if (resultSection) resultSection.classList.remove('cuckoo-hidden'); + console.error("[Cookie Code] 工具执行异常:", err); + const resultSection = document.getElementById("cuckoo-result-section"); + const resultStatus = document.getElementById("cuckoo-result-status"); + const resultOutput = document.getElementById("cuckoo-result-output"); + if (resultSection) resultSection.classList.remove("cuckoo-hidden"); if (resultStatus) { - resultStatus.textContent = t('overlay.status.sysError'); - resultStatus.className = 'cuckoo-result-status error'; + resultStatus.textContent = t("overlay.status.sysError"); + resultStatus.className = "cuckoo-result-status error"; } if (resultOutput) resultOutput.textContent = err.message || String(err); // 系统异常也要回传 AI,让它知道发生了什么 - sendToolResultToChat(toolCall, { success: false, error: t('overlay.output.systemException', { msg: err.message || String(err) }) }); + sendToolResultToChat(toolCall, { + success: false, + error: t("overlay.output.systemException", { + msg: err.message || String(err), + }), + }); } finally { isExecuting = false; setTaskStatus(false); diff --git a/src/preload/i18n/i18n.js b/src/preload/i18n/i18n.js index 8454712..654cbfd 100644 --- a/src/preload/i18n/i18n.js +++ b/src/preload/i18n/i18n.js @@ -9,292 +9,632 @@ * При смене языка рекомендуется location.reload() — просто и надёжно. */ -const DEFAULTS = { ru: '', en: '' }; +const DEFAULTS = { ru: "", en: "" }; // ========== Словарь ========== const KEYS = { // ---- Оверлей: заголовки и общие ---- - 'overlay.title': { ru: 'Cookie Code', en: 'Cookie Code' }, - 'overlay.btn.minimize': { ru: 'Свернуть панель', en: 'Collapse panel' }, - 'overlay.label.currentDir': { ru: 'Текущий каталог проекта', en: 'Current project directory' }, - 'overlay.btn.changeDir': { ru: '🔄 Изменить', en: '🔄 Change' }, - 'overlay.btn.changeDir.title':{ ru: 'Изменить каталог проекта', en: 'Change project directory' }, - 'overlay.dir.notSelected': { ru: 'Не выбрано', en: 'Not selected' }, + "overlay.title": { ru: "Cookie Code", en: "Cookie Code" }, + "overlay.btn.minimize": { ru: "Свернуть панель", en: "Collapse panel" }, + "overlay.label.currentDir": { + ru: "Текущий каталог проекта", + en: "Current project directory", + }, + "overlay.btn.changeDir": { ru: "🔄 Изменить", en: "🔄 Change" }, + "overlay.btn.changeDir.title": { + ru: "Изменить каталог проекта", + en: "Change project directory", + }, + "overlay.dir.notSelected": { ru: "Не выбрано", en: "Not selected" }, // ---- Оверлей: кнопки ---- - 'overlay.btn.init': { ru: 'Инициализировать проект', en: 'Initialize project' }, - 'overlay.btn.init.loading': { ru: '⏳ Инициализация...', en: '⏳ Initializing...' }, - 'overlay.btn.windowManager': { ru: 'Окна', en: 'Windows' }, - 'overlay.btn.windowManager.title': { ru: 'Управление окнами', en: 'Manage windows' }, - 'overlay.btn.mcp': { ru: 'MCP', en: 'MCP' }, - 'overlay.btn.mcp.title': { ru: 'Управление MCP-инструментами', en: 'Manage MCP tools' }, - 'overlay.btn.tg': { ru: 'Telegram', en: 'Telegram' }, - 'overlay.btn.tg.title': { ru: 'Настройки Telegram-бота', en: 'Telegram bot settings' }, - 'tg.title': { ru: 'Telegram-бот', en: 'Telegram bot' }, - 'tg.label.token': { ru: 'Токен бота (@BotFather)', en: 'Bot token (@BotFather)' }, - 'tg.label.chatId': { ru: 'Chat ID', en: 'Chat ID' }, - 'tg.label.enabled': { ru: 'Включить бота', en: 'Enable bot' }, - 'tg.label.notifyTools': { ru: 'Уведомления о tool', en: 'Tool notifications' }, - 'tg.label.chatFeed': { ru: 'Принимать сообщения из TG в чат', en: 'Accept messages from TG into chat' }, - 'tg.btn.save': { ru: 'Сохранить', en: 'Save' }, - 'tg.btn.ping': { ru: 'Проверить токен', en: 'Check token' }, - 'tg.btn.test': { ru: 'Тестовое сообщение', en: 'Test message' }, - 'tg.status.off': { ru: 'Выключен', en: 'Disabled' }, - 'tg.status.on': { ru: 'Работает', en: 'Running' }, - 'overlay.btn.genDoc': { ru: 'Создать описание', en: 'Generate docs' }, - 'overlay.btn.genDoc.title': { ru: 'Попросить AI сгенерировать описание проекта (CUCKOO.md)', en: 'Ask AI to generate a project description (CUCKOO.md)' }, - 'overlay.btn.immersive': { ru: 'Погружение', en: 'Immersive' }, - 'overlay.btn.immersive.title':{ ru: 'Переключить в режим погружения', en: 'Switch to immersive mode' }, - 'overlay.btn.manualParse': { ru: 'Разобрать вручную', en: 'Manual parse' }, - 'overlay.btn.manualParse.title': { ru: 'Вручную разобрать вызовы инструментов в текущем ответе', en: 'Manually parse tool calls in the latest reply' }, - 'overlay.btn.manualParse.loading': { ru: 'Разбор...', en: 'Parsing...' }, - 'overlay.toast.executing': { ru: 'Команда уже выполняется, ручной разбор не нужен', en: 'A command is already running, no manual parse needed' }, - 'overlay.toast.manualParseTriggered': { ru: 'Запущен ручной разбор последнего ответа AI', en: 'Manual parse of the latest AI reply triggered' }, - 'overlay.toast.manualParseError': { ru: 'Ошибка ручного разбора: {msg}', en: 'Manual parse error: {msg}' }, - 'overlay.badge.toolDetected': { ru: 'Cookie Code — обнаружен вызов инструмента', en: 'Cookie Code — tool call detected' }, - 'overlay.badge.jsDetected': { ru: 'Cookie Code — обнаружен JS-скрипт', en: 'Cookie Code — JS tool script detected' }, - 'overlay.toast.execStarted': { ru: 'Начато выполнение команды', en: 'Command execution started' }, - 'overlay.preview.toolPrefix': { ru: '[Инструмент] {name}', en: '[Tool] {name}' }, - 'overlay.preview.params': { ru: 'Параметры: {json}', en: 'Parameters: {json}' }, - 'overlay.preview.jsPrefix': { ru: '[JS-скрипт]', en: '[JS tool script]' }, - 'overlay.status.jsFailed': { ru: '❌ Ошибка выполнения JS-скрипта', en: '❌ JS script execution failed' }, - 'overlay.status.sysError': { ru: '❌ Системная ошибка', en: '❌ System error' }, - 'overlay.status.jsOk': { ru: '✅ JS-скрипт выполнен успешно', en: '✅ JS script executed successfully' }, - 'overlay.status.jsExit': { ru: '⚠ JS-скрипт завершён с кодом {code}', en: '⚠ JS script finished with code {code}' }, - 'overlay.status.toolOk': { ru: '✅ Инструмент {name} выполнен успешно', en: '✅ Tool {name} executed successfully' }, - 'overlay.status.toolFailed': { ru: '❌ Инструмент {name} выполнен с ошибкой', en: '❌ Tool {name} failed' }, - 'overlay.output.scriptDone': { ru: '(скрипт выполнен, без вывода)', en: '(script finished, no output)' }, - 'overlay.output.unknownError': { ru: 'Неизвестная ошибка', en: 'Unknown error' }, - 'overlay.output.execFailed': { ru: 'Ошибка выполнения', en: 'Execution failed' }, - 'toolResult.success': { ru: 'Результат', en: 'Result' }, - 'toolResult.error': { ru: 'Ошибка выполнения', en: 'Execution error' }, - 'toolResult.denied': { ru: 'Отклонено пользователем', en: 'Denied by user' }, - 'overlay.output.systemException': { ru: 'Системное исключение: {msg}', en: 'System exception: {msg}' }, + "overlay.btn.init": { + ru: "Инициализировать проект", + en: "Initialize project", + }, + "overlay.btn.init.loading": { + ru: "⏳ Инициализация...", + en: "⏳ Initializing...", + }, + "overlay.btn.windowManager": { ru: "Окна", en: "Windows" }, + "overlay.btn.windowManager.title": { + ru: "Управление окнами", + en: "Manage windows", + }, + "overlay.btn.mcp": { ru: "MCP", en: "MCP" }, + "overlay.btn.mcp.title": { + ru: "Управление MCP-инструментами", + en: "Manage MCP tools", + }, + "overlay.btn.tg": { ru: "Telegram", en: "Telegram" }, + "overlay.btn.tg.title": { + ru: "Настройки Telegram-бота", + en: "Telegram bot settings", + }, + "tg.title": { ru: "Telegram-бот", en: "Telegram bot" }, + "tg.label.token": { + ru: "Токен бота (@BotFather)", + en: "Bot token (@BotFather)", + }, + "tg.label.chatId": { ru: "Chat ID", en: "Chat ID" }, + "tg.label.enabled": { ru: "Включить бота", en: "Enable bot" }, + "tg.label.notifyTools": { + ru: "Уведомления о tool", + en: "Tool notifications", + }, + "tg.label.chatFeed": { + ru: "Принимать сообщения из TG в чат", + en: "Accept messages from TG into chat", + }, + "tg.btn.save": { ru: "Сохранить", en: "Save" }, + "tg.btn.ping": { ru: "Проверить токен", en: "Check token" }, + "tg.btn.test": { ru: "Тестовое сообщение", en: "Test message" }, + "tg.status.off": { ru: "Выключен", en: "Disabled" }, + "tg.status.on": { ru: "Работает", en: "Running" }, + "overlay.btn.genDoc": { ru: "Создать описание", en: "Generate docs" }, + "overlay.btn.genDoc.title": { + ru: "Попросить AI сгенерировать описание проекта (CUCKOO.md)", + en: "Ask AI to generate a project description (CUCKOO.md)", + }, + "overlay.btn.immersive": { ru: "Погружение", en: "Immersive" }, + "overlay.btn.immersive.title": { + ru: "Переключить в режим погружения", + en: "Switch to immersive mode", + }, + "overlay.btn.manualParse": { ru: "Разобрать вручную", en: "Manual parse" }, + "overlay.btn.manualParse.title": { + ru: "Вручную разобрать вызовы инструментов в текущем ответе", + en: "Manually parse tool calls in the latest reply", + }, + "overlay.btn.manualParse.loading": { ru: "Разбор...", en: "Parsing..." }, + "overlay.toast.executing": { + ru: "Команда уже выполняется, ручной разбор не нужен", + en: "A command is already running, no manual parse needed", + }, + "overlay.toast.manualParseTriggered": { + ru: "Запущен ручной разбор последнего ответа AI", + en: "Manual parse of the latest AI reply triggered", + }, + "overlay.toast.manualParseError": { + ru: "Ошибка ручного разбора: {msg}", + en: "Manual parse error: {msg}", + }, + "overlay.badge.toolDetected": { + ru: "Cookie Code — обнаружен вызов инструмента", + en: "Cookie Code — tool call detected", + }, + "overlay.badge.jsDetected": { + ru: "Cookie Code — обнаружен JS-скрипт", + en: "Cookie Code — JS tool script detected", + }, + "overlay.toast.execStarted": { + ru: "Начато выполнение команды", + en: "Command execution started", + }, + "overlay.preview.toolPrefix": { + ru: "[Инструмент] {name}", + en: "[Tool] {name}", + }, + "overlay.preview.params": { + ru: "Параметры: {json}", + en: "Parameters: {json}", + }, + "overlay.preview.jsPrefix": { ru: "[JS-скрипт]", en: "[JS tool script]" }, + "overlay.status.jsFailed": { + ru: "❌ Ошибка выполнения JS-скрипта", + en: "❌ JS script execution failed", + }, + "overlay.status.sysError": { + ru: "❌ Системная ошибка", + en: "❌ System error", + }, + "overlay.status.jsOk": { + ru: "✅ JS-скрипт выполнен успешно", + en: "✅ JS script executed successfully", + }, + "overlay.status.jsExit": { + ru: "⚠ JS-скрипт завершён с кодом {code}", + en: "⚠ JS script finished with code {code}", + }, + "overlay.status.toolOk": { + ru: "✅ Инструмент {name} выполнен успешно", + en: "✅ Tool {name} executed successfully", + }, + "overlay.status.toolFailed": { + ru: "❌ Инструмент {name} выполнен с ошибкой", + en: "❌ Tool {name} failed", + }, + "overlay.output.scriptDone": { + ru: "(скрипт выполнен, без вывода)", + en: "(script finished, no output)", + }, + "overlay.output.unknownError": { + ru: "Неизвестная ошибка", + en: "Unknown error", + }, + "overlay.output.execFailed": { + ru: "Ошибка выполнения", + en: "Execution failed", + }, + "toolResult.success": { ru: "Результат", en: "Result" }, + "toolResult.error": { ru: "Ошибка выполнения", en: "Execution error" }, + "toolResult.denied": { ru: "Отклонено пользователем", en: "Denied by user" }, + "overlay.output.systemException": { + ru: "Системное исключение: {msg}", + en: "System exception: {msg}", + }, // ---- Оверлей: сессии ---- - 'overlay.label.convTokens': { ru: 'Токены диалога', en: 'Conversation tokens' }, - 'overlay.label.sessions': { ru: 'Сессии', en: 'Sessions' }, - 'overlay.btn.refreshSessions':{ ru: '🔄 Обновить', en: '🔄 Refresh' }, - 'overlay.sessions.empty': { ru: 'Нет сессий', en: 'No sessions' }, + "overlay.label.convTokens": { + ru: "Токены диалога", + en: "Conversation tokens", + }, + "overlay.label.sessions": { ru: "Сессии", en: "Sessions" }, + "overlay.btn.refreshSessions": { ru: "🔄 Обновить", en: "🔄 Refresh" }, + "overlay.sessions.empty": { ru: "Нет сессий", en: "No sessions" }, // ---- Оверлей: задержка отправки ---- - 'overlay.label.sendDelay': { ru: 'Задержка отправки', en: 'Send delay' }, - 'overlay.label.delayRange': { ru: 'до', en: 'to' }, - 'overlay.btn.saveDelay': { ru: 'Сохранить задержку', en: 'Save delay' }, + "overlay.label.sendDelay": { ru: "Задержка отправки", en: "Send delay" }, + "overlay.label.delayRange": { ru: "до", en: "to" }, + "overlay.label.delaySeconds": { ru: "сек", en: "sec" }, + "overlay.btn.saveDelay": { ru: "Сохранить задержку", en: "Save delay" }, + "overlay.delay.saved": { + ru: "Задержка сохранена: {min} - {max} сек", + en: "Delay saved: {min} - {max} sec", + }, + "overlay.delay.errMin": { + ru: "Минимум должен быть неотрицательным числом (сек)", + en: "Min must be a non-negative number (sec)", + }, + "overlay.delay.errMax": { + ru: "Максимум не может быть меньше минимума", + en: "Max cannot be less than min", + }, + "overlay.delay.errLimit": { + ru: "Максимум не может превышать 10 секунд", + en: "Max cannot exceed 10 seconds", + }, // ---- Оверлей: задача/результат/история ---- - 'overlay.label.task': { ru: 'Обнаружена задача:', en: 'Task detected:' }, - 'overlay.task.running': { ru: 'Выполняется', en: 'Running' }, - 'overlay.task.kill': { ru: '⏹ Остановить процесс', en: '⏹ Kill process' }, - 'overlay.task.kill.title': { ru: 'Экстренно завершить все активные дочерние процессы', en: 'Emergency-kill all active child processes' }, - 'overlay.task.killed': { ru: 'Процесс остановлен ({count})', en: 'Process stopped ({count})' }, - 'overlay.task.stopped': { ru: 'Задача остановлена', en: 'Task stopped' }, - 'overlay.task.killNone': { ru: 'Нет активных процессов', en: 'No active processes' }, - 'overlay.task.killError': { ru: 'Не удалось остановить процесс', en: 'Failed to kill process' }, - 'overlay.cmd.none': { ru: 'Нет', en: 'None' }, - 'overlay.label.result': { ru: 'Результат:', en: 'Result:' }, - 'overlay.label.history': { ru: 'История', en: 'History' }, - 'overlay.btn.clearHistory': { ru: 'Очистить историю', en: 'Clear history' }, - 'overlay.history.empty': { ru: 'Пока пусто', en: 'No history yet' }, + "overlay.label.task": { ru: "Обнаружена задача:", en: "Task detected:" }, + "overlay.task.running": { ru: "Выполняется", en: "Running" }, + "overlay.task.kill": { ru: "⏹ Остановить процесс", en: "⏹ Kill process" }, + "overlay.task.kill.title": { + ru: "Экстренно завершить все активные дочерние процессы", + en: "Emergency-kill all active child processes", + }, + "overlay.task.killed": { + ru: "Процесс остановлен ({count})", + en: "Process stopped ({count})", + }, + "overlay.task.stopped": { ru: "Задача остановлена", en: "Task stopped" }, + "overlay.task.killNone": { + ru: "Нет активных процессов", + en: "No active processes", + }, + "overlay.task.killError": { + ru: "Не удалось остановить процесс", + en: "Failed to kill process", + }, + "overlay.cmd.none": { ru: "Нет", en: "None" }, + "overlay.label.result": { ru: "Результат:", en: "Result:" }, + "overlay.label.history": { ru: "История", en: "History" }, + "overlay.btn.clearHistory": { ru: "Очистить историю", en: "Clear history" }, + "overlay.history.empty": { ru: "Пока пусто", en: "No history yet" }, // ---- Оверлей: window manager ---- - 'wm.title': { ru: 'Управление окнами', en: 'Window manager' }, - 'wm.btn.close': { ru: 'Закрыть', en: 'Close' }, - 'wm.btn.newWindow': { ru: 'Новое окно', en: 'New window' }, - 'wm.label.list': { ru: 'Окна', en: 'Windows' }, - 'wm.btn.refresh': { ru: '🔄 Обновить', en: '🔄 Refresh' }, - 'wm.empty': { ru: 'Нет окон', en: 'No windows' }, - 'wm.toast.created': { ru: 'Создано новое окно DeepSeek', en: 'New DeepSeek window created' }, + "wm.title": { ru: "Управление окнами", en: "Window manager" }, + "wm.btn.close": { ru: "Закрыть", en: "Close" }, + "wm.btn.newWindow": { ru: "Новое окно", en: "New window" }, + "wm.label.list": { ru: "Окна", en: "Windows" }, + "wm.btn.refresh": { ru: "🔄 Обновить", en: "🔄 Refresh" }, + "wm.empty": { ru: "Нет окон", en: "No windows" }, + "wm.toast.created": { + ru: "Создано новое окно DeepSeek", + en: "New DeepSeek window created", + }, // ---- Оверлей: MCP ---- - 'mcp.title': { ru: 'MCP-инструменты', en: 'MCP tools' }, - 'mcp.label.configured': { ru: 'Настроены', en: 'Configured' }, - 'mcp.empty': { ru: 'Нет', en: 'None' }, - 'mcp.btn.save': { ru: 'Сохранить настройки', en: 'Save config' }, + "mcp.title": { ru: "MCP-инструменты", en: "MCP tools" }, + "mcp.label.configured": { ru: "Настроены", en: "Configured" }, + "mcp.empty": { ru: "Нет", en: "None" }, + "mcp.btn.save": { ru: "Сохранить настройки", en: "Save config" }, // ---- Оверлей: бейдж и первый диалог ---- - 'badge.title': { ru: 'Cookie Code работает', en: 'Cookie Code is running' }, - 'firstTime.text': { ru: 'При первом создании диалога нужно инициализировать проект и выбрать каталог, иначе работа невозможна', en: 'When starting the first conversation, initialize a project and pick a directory — otherwise the app cannot work' }, - 'init.success.text': { ru: 'Проект успешно инициализирован!', en: 'Project initialized successfully!' }, - 'init.success.btn': { ru: 'Отлично', en: 'Great' }, + "badge.title": { ru: "Cookie Code работает", en: "Cookie Code is running" }, + "firstTime.text": { + ru: "При первом создании диалога нужно инициализировать проект и выбрать каталог, иначе работа невозможна", + en: "When starting the first conversation, initialize a project and pick a directory — otherwise the app cannot work", + }, + "init.success.text": { + ru: "Проект успешно инициализирован!", + en: "Project initialized successfully!", + }, + "init.success.btn": { ru: "Отлично", en: "Great" }, // ---- Настройки: общее ---- - 'settings.title': { ru: 'Cookie Code', en: 'Cookie Code' }, - 'settings.subtitle': { ru: 'Настройки интерфейса и фонового изображения', en: 'Interface and background settings' }, - 'settings.section.customization': { ru: 'Кастомизация Cookie Code', en: 'Cookie Code customization' }, - 'settings.customization.enabled': { ru: 'Кастомизация включена', en: 'Customization enabled' }, - 'settings.customization.disabled': { ru: 'Кастомизация выключена', en: 'Customization disabled' }, - 'settings.customization.enable': { ru: 'Включить всё', en: 'Enable everything' }, - 'settings.customization.disable': { ru: 'Выключить всё', en: 'Disable everything' }, - 'settings.customization.reloading': { ru: 'Применение...', en: 'Applying...' }, - 'settings.section.blur': { ru: 'Размытие', en: 'Blur' }, - 'settings.section.opacity': { ru: 'Прозрачность панелей', en: 'Panel opacity' }, - 'settings.section.effects': { ru: 'Эффекты', en: 'Effects' }, - 'settings.section.inputGlass': { ru: 'Стеклянное поле ввода', en: 'Glass input field' }, - 'settings.inputGlass.hint': { ru: 'Матовое стекло для поля ввода сообщения DeepSeek', en: 'Frosted glass for the DeepSeek message input field' }, - 'settings.inputGlass.blur': { ru: 'Размытие поля ввода (стекло)', en: 'Input field blur (glass)' }, - 'settings.inputGlass.opacity': { ru: 'Плотность фона поля ввода', en: 'Input field background density' }, - 'settings.inputGlass.enabled': { ru: 'Стекло поля ввода включено', en: 'Input field glass enabled' }, - 'settings.section.dangerous': { ru: 'Опасные команды (regex, по одной на строку)', en: 'Dangerous commands (regex, one per line)' }, - 'settings.section.background':{ ru: 'Фон страницы', en: 'Page background' }, - 'settings.bg.openFolder': { ru: 'Открыть папку фонов', en: 'Open backgrounds folder' }, - 'settings.bg.refresh': { ru: 'Обновить', en: 'Refresh' }, - 'settings.bg.hint': { ru: 'Свои картинки: положите файлы (.webp/.jpg/.png/.gif) в папку и нажмите «Обновить».', en: 'Custom images: drop files (.webp/.jpg/.png/.gif) into the folder and click Refresh.' }, - 'settings.section.language': { ru: 'Язык', en: 'Language' }, + "settings.title": { ru: "Cookie Code", en: "Cookie Code" }, + "settings.subtitle": { + ru: "Настройки интерфейса и фонового изображения", + en: "Interface and background settings", + }, + "settings.section.customization": { + ru: "Кастомизация Cookie Code", + en: "Cookie Code customization", + }, + "settings.customization.enabled": { + ru: "Кастомизация включена", + en: "Customization enabled", + }, + "settings.customization.disabled": { + ru: "Кастомизация выключена", + en: "Customization disabled", + }, + "settings.customization.enable": { + ru: "Включить всё", + en: "Enable everything", + }, + "settings.customization.disable": { + ru: "Выключить всё", + en: "Disable everything", + }, + "settings.customization.reloading": { + ru: "Применение...", + en: "Applying...", + }, + "settings.section.blur": { ru: "Размытие", en: "Blur" }, + "settings.section.opacity": { + ru: "Прозрачность панелей", + en: "Panel opacity", + }, + "settings.section.effects": { ru: "Эффекты", en: "Effects" }, + "settings.section.inputGlass": { + ru: "Стеклянное поле ввода", + en: "Glass input field", + }, + "settings.inputGlass.hint": { + ru: "Матовое стекло для поля ввода сообщения DeepSeek", + en: "Frosted glass for the DeepSeek message input field", + }, + "settings.inputGlass.blur": { + ru: "Размытие поля ввода (стекло)", + en: "Input field blur (glass)", + }, + "settings.inputGlass.opacity": { + ru: "Плотность фона поля ввода", + en: "Input field background density", + }, + "settings.inputGlass.enabled": { + ru: "Стекло поля ввода включено", + en: "Input field glass enabled", + }, + "settings.section.dangerous": { + ru: "Опасные команды (regex, по одной на строку)", + en: "Dangerous commands (regex, one per line)", + }, + "settings.section.background": { ru: "Фон страницы", en: "Page background" }, + "settings.bg.openFolder": { + ru: "Открыть папку фонов", + en: "Open backgrounds folder", + }, + "settings.bg.refresh": { ru: "Обновить", en: "Refresh" }, + "settings.bg.hint": { + ru: "Свои картинки: положите файлы (.webp/.jpg/.png/.gif) в папку и нажмите «Обновить».", + en: "Custom images: drop files (.webp/.jpg/.png/.gif) into the folder and click Refresh.", + }, + "settings.section.language": { ru: "Язык", en: "Language" }, // ---- Категории / подвкладки настроек ---- - 'settings.tab.theme': { ru: '🎨 Тема и стекло', en: '🎨 Theme & Glass' }, - 'settings.tab.overlay': { ru: '🪟 Панель Cookie', en: '🪟 Cookie Panel' }, - 'settings.tab.bg': { ru: '🖼 Фон страницы', en: '🖼 Page Background' }, - 'settings.tab.telegram': { ru: '🤖 Telegram-бот', en: '🤖 Telegram Bot' }, - 'settings.tab.system': { ru: '⚙️ Система и безопасность', en: '⚙️ System & Security' }, - 'settings.section.maintenance':{ ru: 'Обслуживание и сброс', en: 'Maintenance & Reset' }, - 'settings.section.service': { ru: 'Сервис', en: 'Service' }, + "settings.tab.theme": { ru: "🎨 Тема и стекло", en: "🎨 Theme & Glass" }, + "settings.tab.overlay": { ru: "🪟 Панель Cookie", en: "🪟 Cookie Panel" }, + "settings.tab.bg": { ru: "🖼 Фон страницы", en: "🖼 Page Background" }, + "settings.tab.telegram": { ru: "🤖 Telegram-бот", en: "🤖 Telegram Bot" }, + "settings.tab.system": { + ru: "⚙️ Система и безопасность", + en: "⚙️ System & Security", + }, + "settings.section.maintenance": { + ru: "Обслуживание и сброс", + en: "Maintenance & Reset", + }, + "settings.section.service": { ru: "Сервис", en: "Service" }, // ---- Настройки: диагностика интеграции ---- - 'settings.section.diagnostics': { ru: 'Диагностика интеграции', en: 'Integration diagnostics' }, - 'settings.diagnostics.title': { ru: 'Проверка селекторов и методов провайдера', en: 'Check provider selectors and methods' }, - 'settings.diagnostics.hint': { ru: 'Если после обновления DeepSeek что-то сломалось — запустите диагностику и скопируйте отчёт в issue.', en: 'If something breaks after a DeepSeek update — run diagnostics and paste the report into an issue.' }, - 'settings.diagnostics.run': { ru: 'Запустить диагностику', en: 'Run diagnostics' }, - 'settings.diagnostics.running': { ru: 'Проверка...', en: 'Checking...' }, - 'settings.diagnostics.modalTitle': { ru: 'Отчёт диагностики', en: 'Diagnostics report' }, - 'settings.diagnostics.copy': { ru: '📋 Копировать отчёт', en: '📋 Copy report' }, - 'settings.diagnostics.copied': { ru: '✅ Скопировано', en: '✅ Copied' }, + "settings.section.diagnostics": { + ru: "Диагностика интеграции", + en: "Integration diagnostics", + }, + "settings.diagnostics.title": { + ru: "Проверка селекторов и методов провайдера", + en: "Check provider selectors and methods", + }, + "settings.diagnostics.hint": { + ru: "Если после обновления DeepSeek что-то сломалось — запустите диагностику и скопируйте отчёт в issue.", + en: "If something breaks after a DeepSeek update — run diagnostics and paste the report into an issue.", + }, + "settings.diagnostics.run": { + ru: "Запустить диагностику", + en: "Run diagnostics", + }, + "settings.diagnostics.running": { ru: "Проверка...", en: "Checking..." }, + "settings.diagnostics.modalTitle": { + ru: "Отчёт диагностики", + en: "Diagnostics report", + }, + "settings.diagnostics.copy": { + ru: "📋 Копировать отчёт", + en: "📋 Copy report", + }, + "settings.diagnostics.copied": { ru: "✅ Скопировано", en: "✅ Copied" }, // ---- Настройки: слайдеры ---- - 'settings.blur.bg': { ru: 'Размытие фонового изображения', en: 'Background image blur' }, - 'settings.blur.header': { ru: 'Размытие шапки (стекло)', en: 'Header blur (glass)' }, - 'settings.blur.sidebar': { ru: 'Размытие сайдбара (стекло)', en: 'Sidebar blur (glass)' }, - 'settings.blur.toolblock': { ru: 'Размытие tool-блоков (стекло)', en: 'Tool block blur (glass)' }, - 'settings.opacity.header': { ru: 'Прозрачность шапки', en: 'Header opacity' }, - 'settings.opacity.sidebar': { ru: 'Прозрачность сайдбара', en: 'Sidebar opacity' }, - 'settings.opacity.toolblock': { ru: 'Прозрачность tool-блоков', en: 'Tool block opacity' }, + "settings.blur.bg": { + ru: "Размытие фонового изображения", + en: "Background image blur", + }, + "settings.blur.header": { + ru: "Размытие шапки (стекло)", + en: "Header blur (glass)", + }, + "settings.blur.sidebar": { + ru: "Размытие сайдбара (стекло)", + en: "Sidebar blur (glass)", + }, + "settings.blur.toolblock": { + ru: "Размытие tool-блоков (стекло)", + en: "Tool block blur (glass)", + }, + "settings.opacity.header": { ru: "Прозрачность шапки", en: "Header opacity" }, + "settings.opacity.sidebar": { + ru: "Прозрачность сайдбара", + en: "Sidebar opacity", + }, + "settings.opacity.toolblock": { + ru: "Прозрачность tool-блоков", + en: "Tool block opacity", + }, // ---- Настройки: панель Cookie Code ---- - 'settings.section.overlay': { ru: 'Панель Cookie Code', en: 'Cookie Code Panel' }, - 'settings.overlay.opacity': { ru: 'Прозрачность фона панели', en: 'Panel background opacity' }, - 'settings.overlay.blur': { ru: 'Размытие панели (стекло)', en: 'Panel blur (glass)' }, - 'settings.overlay.width': { ru: 'Ширина панели', en: 'Panel width' }, - 'settings.overlay.bgColor': { ru: 'Цвет фона панели', en: 'Panel background color' }, - 'settings.overlay.btnColor': { ru: 'Основной цвет кнопок', en: 'Primary button color' }, - 'settings.overlay.btnRadius': { ru: 'Скругление кнопок панели', en: 'Button corner radius' }, + "settings.section.overlay": { + ru: "Панель Cookie Code", + en: "Cookie Code Panel", + }, + "settings.overlay.opacity": { + ru: "Прозрачность фона панели", + en: "Panel background opacity", + }, + "settings.overlay.blur": { + ru: "Размытие панели (стекло)", + en: "Panel blur (glass)", + }, + "settings.overlay.width": { ru: "Ширина панели", en: "Panel width" }, + "settings.overlay.bgColor": { + ru: "Цвет фона панели", + en: "Panel background color", + }, + "settings.overlay.btnColor": { + ru: "Основной цвет кнопок", + en: "Primary button color", + }, + "settings.overlay.btnRadius": { + ru: "Скругление кнопок панели", + en: "Button corner radius", + }, // ---- Настройки: эффекты ---- - 'settings.effect.rgb': { ru: 'RGB-переливание ника', en: 'RGB animated username' }, + "settings.effect.rgb": { + ru: "RGB-переливание ника", + en: "RGB animated username", + }, // ---- Настройки: опасные команды ---- - 'settings.dangerous.hint': { ru: 'Пустой список = все команды разрешены', en: 'Empty list = all commands allowed' }, - 'settings.dangerous.save': { ru: 'Сохранить', en: 'Save' }, - 'settings.dangerous.saving': { ru: 'Сохранение...', en: 'Saving...' }, - 'settings.dangerous.saved': { ru: '✅ Сохранено', en: '✅ Saved' }, - 'settings.dangerous.error': { ru: '❌ Ошибка', en: '❌ Error' }, + "settings.dangerous.hint": { + ru: "Пустой список = все команды разрешены", + en: "Empty list = all commands allowed", + }, + "settings.dangerous.save": { ru: "Сохранить", en: "Save" }, + "settings.dangerous.saving": { ru: "Сохранение...", en: "Saving..." }, + "settings.dangerous.saved": { ru: "✅ Сохранено", en: "✅ Saved" }, + "settings.dangerous.error": { ru: "❌ Ошибка", en: "❌ Error" }, // ---- Экспорт ответа AI ---- - 'export.btn.title': { ru: 'Экспорт ответа', en: 'Export response' }, - 'export.menu.pdf': { ru: '📄 Скачать PDF', en: '📄 Download PDF' }, - 'export.menu.docx': { ru: '📝 Скачать DOCX', en: '📝 Download DOCX' }, - 'export.status.working': { ru: 'Сохранение...', en: 'Saving...' }, - 'export.status.ok': { ru: '✅ Сохранено', en: '✅ Saved' }, - 'export.status.err': { ru: '❌ Ошибка', en: '❌ Error' }, + "export.btn.title": { ru: "Экспорт ответа", en: "Export response" }, + "export.menu.pdf": { ru: "📄 Скачать PDF", en: "📄 Download PDF" }, + "export.menu.docx": { ru: "📝 Скачать DOCX", en: "📝 Download DOCX" }, + "export.status.working": { ru: "Сохранение...", en: "Saving..." }, + "export.status.ok": { ru: "✅ Сохранено", en: "✅ Saved" }, + "export.status.err": { ru: "❌ Ошибка", en: "❌ Error" }, // ---- Настройки: кнопки внизу ---- - 'settings.btn.openConfig': { ru: 'Открыть файл настроек', en: 'Open configuration file' }, - 'settings.btn.openConfig.title': { ru: 'Открыть cuckoo-settings.json в системном редакторе', en: 'Open cuckoo-settings.json in the system editor' }, - 'settings.btn.openConfig.opened': { ru: '✅ Файл открыт', en: '✅ File opened' }, - 'settings.btn.openConfig.error': { ru: '❌ Не удалось открыть', en: '❌ Failed to open' }, - 'settings.btn.clearStorage': { ru: 'Очистить мета-данные', en: 'Clear meta data' }, - 'settings.btn.clearStorage.title': { ru: 'Очистить сохранённые мета-данные и историю ошибок', en: 'Clear saved meta data and error history' }, - 'settings.btn.reset': { ru: 'Сбросить настройки', en: 'Reset settings' }, - 'settings.btn.resetting': { ru: 'Сброс...', en: 'Resetting...' }, - 'settings.btn.clearing': { ru: 'Очистка...', en: 'Clearing...' }, + "settings.btn.openConfig": { + ru: "Открыть файл настроек", + en: "Open configuration file", + }, + "settings.btn.openConfig.title": { + ru: "Открыть cuckoo-settings.json в системном редакторе", + en: "Open cuckoo-settings.json in the system editor", + }, + "settings.btn.openConfig.opened": { + ru: "✅ Файл открыт", + en: "✅ File opened", + }, + "settings.btn.openConfig.error": { + ru: "❌ Не удалось открыть", + en: "❌ Failed to open", + }, + "settings.btn.clearStorage": { + ru: "Очистить мета-данные", + en: "Clear meta data", + }, + "settings.btn.clearStorage.title": { + ru: "Очистить сохранённые мета-данные и историю ошибок", + en: "Clear saved meta data and error history", + }, + "settings.btn.reset": { ru: "Сбросить настройки", en: "Reset settings" }, + "settings.btn.resetting": { ru: "Сброс...", en: "Resetting..." }, + "settings.btn.clearing": { ru: "Очистка...", en: "Clearing..." }, // ---- Настройки: язык ---- - 'settings.lang.ru': { ru: 'Русский', en: 'Russian' }, - 'settings.lang.en': { ru: 'Английский', en: 'English' }, + "settings.lang.ru": { ru: "Русский", en: "Russian" }, + "settings.lang.en": { ru: "Английский", en: "English" }, // ---- Slash-команды ---- - 'cmd.menu.title': { ru: 'Команды', en: 'Commands' }, - 'cmd.menu.files': { ru: 'Файлы проекта', en: 'Project files' }, - 'cmd.review.description': { ru: 'Проверить текущие изменения проекта', en: 'Review current project changes' }, - 'cmd.summarize.description': { ru: 'Сделать краткий итог текущей сессии', en: 'Summarize the current session' }, - 'plan.toggle.label': { ru: 'План', en: 'Plan' }, - 'plan.dialog.title': { ru: 'План на утверждение', en: 'Plan for approval' }, - 'plan.dialog.deny': { ru: 'Отказать в плане', en: 'Reject plan' }, - 'plan.dialog.approve': { ru: 'Согласиться', en: 'Approve' }, - 'plan.approve.prompt': { ru: 'Работай в соответствии с планом', en: 'Work according to the plan' }, + "cmd.menu.title": { ru: "Команды", en: "Commands" }, + "cmd.menu.files": { ru: "Файлы проекта", en: "Project files" }, + "cmd.review.description": { + ru: "Проверить текущие изменения проекта", + en: "Review current project changes", + }, + "cmd.summarize.description": { + ru: "Сделать краткий итог текущей сессии", + en: "Summarize the current session", + }, + "plan.toggle.label": { ru: "План", en: "Plan" }, + "plan.dialog.title": { ru: "План на утверждение", en: "Plan for approval" }, + "plan.dialog.deny": { ru: "Отказать в плане", en: "Reject plan" }, + "plan.dialog.approve": { ru: "Согласиться", en: "Approve" }, + "plan.approve.prompt": { + ru: "Работай в соответствии с планом", + en: "Work according to the plan", + }, // ---- Подтверждение tool-вызовов (approval gate) ---- - 'approval.title.tool': { ru: 'Подтверждение вызова инструмента', en: 'Tool call approval' }, - 'approval.title.js': { ru: 'Подтверждение JS-скрипта', en: 'JS script approval' }, - 'approval.subtitle': { ru: 'Cookie Code запрашивает разрешение перед выполнением', en: 'Cookie Code asks for permission before executing' }, - 'approval.label.params': { ru: 'Параметры', en: 'Parameters' }, - 'approval.label.code': { ru: 'Код', en: 'Code' }, - 'approval.name.js': { ru: '[JS-скрипт]', en: '[JS script]' }, - 'approval.approve': { ru: 'Разрешить (Enter)', en: 'Approve (Enter)' }, - 'approval.always': { ru: 'Разрешать «{name}» до перезагрузки', en: 'Always allow "{name}" (this session)' }, - 'approval.deny': { ru: 'Отклонить (Esc)', en: 'Deny (Esc)' }, + "approval.title.tool": { + ru: "Подтверждение вызова инструмента", + en: "Tool call approval", + }, + "approval.title.js": { + ru: "Подтверждение JS-скрипта", + en: "JS script approval", + }, + "approval.subtitle": { + ru: "Cookie Code запрашивает разрешение перед выполнением", + en: "Cookie Code asks for permission before executing", + }, + "approval.label.params": { ru: "Параметры", en: "Parameters" }, + "approval.label.code": { ru: "Код", en: "Code" }, + "approval.name.js": { ru: "[JS-скрипт]", en: "[JS script]" }, + "approval.approve": { ru: "Разрешить (Enter)", en: "Approve (Enter)" }, + "approval.always": { + ru: "Разрешать «{name}» до перезагрузки", + en: 'Always allow "{name}" (this session)', + }, + "approval.deny": { ru: "Отклонить (Esc)", en: "Deny (Esc)" }, // ---- Настройки: агент и приватность ---- - 'settings.section.agent': { ru: 'Агент и приватность', en: 'Agent & Privacy' }, - 'settings.approval.off': { ru: 'Выкл', en: 'Off' }, - 'settings.approval.risky': { ru: 'Рискованные', en: 'Risky only' }, - 'settings.approval.all': { ru: 'Все вызовы', en: 'All calls' }, - 'settings.approval.hint': { ru: 'Запрашивать подтверждение перед выполнением: рискованные инструменты (bash, запись файлов, SQL) или все вызовы', en: 'Ask before executing: risky tools (bash, file writes, SQL) or every call' }, - 'settings.approval.saved': { ru: '✅ Режим сохранён', en: '✅ Mode saved' }, - 'settings.hideSystemMessages': { ru: 'Скрывать служебные сообщения в чате', en: 'Hide service messages in chat' }, - 'settings.hideSystemMessages.hint': { ru: 'Результаты инструментов, JS-сводки и системный промпт по-прежнему уходят в AI, но не отображаются в чате', en: 'Tool results, JS digests and the system prompt still reach the AI but stay invisible in the chat' }, - 'settings.fileChip': { ru: 'Файловые пути как кликабельные чипы', en: 'File paths as clickable chips' }, - 'settings.fileChip.hint': { ru: 'Абсолютные пути (C:\\…, D:\\…) превращаются в чипы. Клик открывает файл в VS Code (или в проводнике)', en: 'Absolute paths (C:\\…, D:\\…) become chips. Click opens the file in VS Code (or in explorer)' }, - 'settings.showProducedFiles': { ru: 'Показывать затронутые файлы под ответом', en: 'Show affected files under AI reply' }, - 'settings.showProducedFiles.hint': { ru: 'Блок «Затронуто # файл» под каждым ответом AI — только успешные write/edit/delete', en: '“Affected # file” block under each AI reply — only successful write/edit/delete' }, - 'settings.showConvTokens': { ru: 'Показывать токены диалога', en: 'Show dialogue tokens' }, - 'settings.showConvTokens.hint': { ru: 'Блок «Токены диалога» в панели Cookie Code (по умолчанию скрыт)', en: '“Dialogue tokens” block in the Cookie Code panel (hidden by default)' }, - 'settings.formatters': { ru: 'Авто-форматирование после write/edit', en: 'Auto-format after write/edit' }, - 'settings.formatters.hint': { ru: 'prettier, biome, gofmt, ruff, rustfmt, shfmt, clang-format — по расширению и конфигу проекта', en: 'prettier, biome, gofmt, ruff, rustfmt, shfmt, clang-format — by file extension and project config' }, + "settings.section.agent": { + ru: "Агент и приватность", + en: "Agent & Privacy", + }, + "settings.approval.off": { ru: "Выкл", en: "Off" }, + "settings.approval.risky": { ru: "Рискованные", en: "Risky only" }, + "settings.approval.all": { ru: "Все вызовы", en: "All calls" }, + "settings.approval.hint": { + ru: "Запрашивать подтверждение перед выполнением: рискованные инструменты (bash, запись файлов, SQL) или все вызовы", + en: "Ask before executing: risky tools (bash, file writes, SQL) or every call", + }, + "settings.approval.saved": { ru: "✅ Режим сохранён", en: "✅ Mode saved" }, + "settings.hideSystemMessages": { + ru: "Скрывать служебные сообщения в чате", + en: "Hide service messages in chat", + }, + "settings.hideSystemMessages.hint": { + ru: "Результаты инструментов, JS-сводки и системный промпт по-прежнему уходят в AI, но не отображаются в чате", + en: "Tool results, JS digests and the system prompt still reach the AI but stay invisible in the chat", + }, + "settings.fileChip": { + ru: "Файловые пути как кликабельные чипы", + en: "File paths as clickable chips", + }, + "settings.fileChip.hint": { + ru: "Абсолютные пути (C:\\…, D:\\…) превращаются в чипы. Клик открывает файл в VS Code (или в проводнике)", + en: "Absolute paths (C:\\…, D:\\…) become chips. Click opens the file in VS Code (or in explorer)", + }, + "settings.showProducedFiles": { + ru: "Показывать затронутые файлы под ответом", + en: "Show affected files under AI reply", + }, + "settings.showProducedFiles.hint": { + ru: "Блок «Затронуто # файл» под каждым ответом AI — только успешные write/edit/delete", + en: "“Affected # file” block under each AI reply — only successful write/edit/delete", + }, + "settings.showConvTokens": { + ru: "Показывать токены диалога", + en: "Show dialogue tokens", + }, + "settings.showConvTokens.hint": { + ru: "Блок «Токены диалога» в панели Cookie Code (по умолчанию скрыт)", + en: "“Dialogue tokens” block in the Cookie Code panel (hidden by default)", + }, + "settings.formatters": { + ru: "Авто-форматирование после write/edit", + en: "Auto-format after write/edit", + }, + "settings.formatters.hint": { + ru: "prettier, biome, gofmt, ruff, rustfmt, shfmt, clang-format — по расширению и конфигу проекта", + en: "prettier, biome, gofmt, ruff, rustfmt, shfmt, clang-format — by file extension and project config", + }, // ---- Diff-панель ---- - 'diff.btn.title': { ru: 'Показать diff изменённых файлов', en: 'Show diff of changed files' }, - 'diff.title': { ru: 'Изменения (git)', en: 'Changes (git)' }, - 'diff.btn.refresh': { ru: 'Обновить', en: 'Refresh' }, - 'diff.btn.close': { ru: 'Закрыть', en: 'Close' }, - 'diff.tab.changes': { ru: 'Изменения', en: 'Changes' }, - 'diff.tab.history': { ru: 'История', en: 'History' }, - 'diff.loading': { ru: 'Загрузка…', en: 'Loading…' }, - 'diff.commit.back': { ru: '← Назад', en: '← Back' }, - 'diff.commit.back.title': { ru: 'Назад к истории', en: 'Back to history' }, - 'diff.commit.full': { ru: 'Весь коммит', en: 'Full commit' }, - 'diff.commit.full.title': { ru: 'Показать весь коммит', en: 'Show the full commit' }, - 'diff.commit.titlePrefix': { ru: 'Весь коммит ', en: 'Full commit ' }, - 'diff.empty': { ru: 'Пустой diff', en: 'Empty diff' }, - 'diff.noChanges': { ru: 'Нет изменённых файлов', en: 'No changed files' }, - 'diff.noCommits': { ru: 'Нет коммитов', en: 'No commits' }, - 'diff.noFiles': { ru: 'Нет файлов', en: 'No files' }, - 'diff.gitNotFound': { ru: 'git не найден', en: 'git not found' }, - 'diff.diffFailed': { ru: 'Не удалось получить diff', en: 'Failed to fetch diff' }, - 'diff.error': { ru: 'Ошибка', en: 'Error' }, - 'diff.errorPrefix': { ru: 'Ошибка: {msg}', en: 'Error: {msg}' }, + "diff.btn.title": { + ru: "Показать diff изменённых файлов", + en: "Show diff of changed files", + }, + "diff.title": { ru: "Изменения (git)", en: "Changes (git)" }, + "diff.btn.refresh": { ru: "Обновить", en: "Refresh" }, + "diff.btn.close": { ru: "Закрыть", en: "Close" }, + "diff.tab.changes": { ru: "Изменения", en: "Changes" }, + "diff.tab.history": { ru: "История", en: "History" }, + "diff.loading": { ru: "Загрузка…", en: "Loading…" }, + "diff.commit.back": { ru: "← Назад", en: "← Back" }, + "diff.commit.back.title": { ru: "Назад к истории", en: "Back to history" }, + "diff.commit.full": { ru: "Весь коммит", en: "Full commit" }, + "diff.commit.full.title": { + ru: "Показать весь коммит", + en: "Show the full commit", + }, + "diff.commit.titlePrefix": { ru: "Весь коммит ", en: "Full commit " }, + "diff.empty": { ru: "Пустой diff", en: "Empty diff" }, + "diff.noChanges": { ru: "Нет изменённых файлов", en: "No changed files" }, + "diff.noCommits": { ru: "Нет коммитов", en: "No commits" }, + "diff.noFiles": { ru: "Нет файлов", en: "No files" }, + "diff.gitNotFound": { ru: "git не найден", en: "git not found" }, + "diff.diffFailed": { + ru: "Не удалось получить diff", + en: "Failed to fetch diff", + }, + "diff.error": { ru: "Ошибка", en: "Error" }, + "diff.errorPrefix": { ru: "Ошибка: {msg}", en: "Error: {msg}" }, // ---- Todo-панель ---- - 'todo.btn.hide': { ru: 'Скрыть', en: 'Hide' }, - 'todo.btn.show': { ru: 'Показать задачи', en: 'Show tasks' }, + "todo.btn.hide": { ru: "Скрыть", en: "Hide" }, + "todo.btn.show": { ru: "Показать задачи", en: "Show tasks" }, // ---- Мета под ответом AI (время / токены / затронутые файлы) ---- - 'meta.time.title': { ru: 'Время ответа', en: 'Response time' }, - 'meta.tokens.title': { ru: 'Оценка токенов в тексте этого ответа', en: 'Estimated tokens in this response' }, - 'meta.produced': { ru: 'Затронуто', en: 'Affected' }, - 'meta.produced.title': { ru: 'Файлы, затронутые за этот ответ', en: 'Files touched in this response' }, + "meta.time.title": { ru: "Время ответа", en: "Response time" }, + "meta.tokens.title": { + ru: "Оценка токенов в тексте этого ответа", + en: "Estimated tokens in this response", + }, + "meta.produced": { ru: "Затронуто", en: "Affected" }, + "meta.produced.title": { + ru: "Файлы, затронутые за этот ответ", + en: "Files touched in this response", + }, }; // ========== Состояние ========== -let currentLang = 'ru'; +let currentLang = "ru"; -const RU_LIKE = ['ru', 'uk', 'be', 'kk', 'ky', 'uz', 'tg', 'hy', 'az', 'mo']; +const RU_LIKE = ["ru", "uk", "be", "kk", "ky", "uz", "tg", "hy", "az", "mo"]; /** * Привести произвольную локаль ('ru-RU', 'uk', 'en-US', ...) к 'ru' | 'en'. * Русскоязычные и близкие локали → 'ru', всё остальное → 'en'. */ function normalizeLang(lang) { - const code = String(lang || '').toLowerCase().split(/[-_]/)[0]; - return RU_LIKE.includes(code) ? 'ru' : 'en'; + const code = String(lang || "") + .toLowerCase() + .split(/[-_]/)[0]; + return RU_LIKE.includes(code) ? "ru" : "en"; } /** @@ -305,13 +645,13 @@ function normalizeLang(lang) { function t(key, params) { const entry = KEYS[key]; if (!entry) { - console.warn('[Cookie Code] i18n: missing key:', key); + console.warn("[Cookie Code] i18n: missing key:", key); return key; } let str = entry[currentLang] || entry.ru || entry.en || key; if (params) { for (const k of Object.keys(params)) { - str = str.replace(new RegExp('\\{' + k + '\\}', 'g'), String(params[k])); + str = str.replace(new RegExp("\\{" + k + "\\}", "g"), String(params[k])); } } return str; @@ -326,7 +666,9 @@ function getLanguage() { */ function setLanguage(lang) { currentLang = normalizeLang(lang); - try { window.__cuckooI18nLang = currentLang; } catch (_) {} + try { + window.__cuckooI18nLang = currentLang; + } catch (_) {} } /** @@ -334,13 +676,20 @@ function setLanguage(lang) { */ async function loadLanguage() { try { - if (!window.electronAPI || typeof window.electronAPI.getCuckooSettings !== 'function') { + if ( + !window.electronAPI || + typeof window.electronAPI.getCuckooSettings !== "function" + ) { return currentLang; } const s = await window.electronAPI.getCuckooSettings(); currentLang = normalizeLang(s && s.language); - } catch (_) { /* оставляем ru по умолчанию */ } - try { window.__cuckooI18nLang = currentLang; } catch (_) {} + } catch (_) { + /* оставляем ru по умолчанию */ + } + try { + window.__cuckooI18nLang = currentLang; + } catch (_) {} return currentLang; } diff --git a/src/preload/overlay/events.js b/src/preload/overlay/events.js index 84f089d..6c0b1bd 100644 --- a/src/preload/overlay/events.js +++ b/src/preload/overlay/events.js @@ -2,21 +2,39 @@ * 覆盖层按钮事件绑定 * 由原 preload.js 拆分而来,逻辑保持不变。 */ -const state = require('../dom/state'); -const { hideOverlay, showOverlay, renderHistory, commandHistory, showToast, showConfirmDialog, hideFirstTimeDialog, handleKillProcess } = require('./ui'); -const { toggleDiffPanel, closeDiffPanel, renderDiffList, closeDiffViewer, setActiveTab, backToLog, openCommitFullDiff } = require('./diff-panel'); -const todoPanel = require('./todo-panel'); -const { handleInitProject, renderSessions } = require('../dom/session-list'); -const { handleManualParse } = require('../dom/observer'); -const { sendToChat } = require('../dom/chat-input'); -const { getProviderByUrl } = require('../../../src/providers'); -const { estimateTokens } = require('../dom/token-estimator'); +const state = require("../dom/state"); +const { t } = require("../i18n/i18n"); +const { + hideOverlay, + showOverlay, + renderHistory, + commandHistory, + showToast, + showConfirmDialog, + hideFirstTimeDialog, + handleKillProcess, +} = require("./ui"); +const { + toggleDiffPanel, + closeDiffPanel, + renderDiffList, + closeDiffViewer, + setActiveTab, + backToLog, + openCommitFullDiff, +} = require("./diff-panel"); +const todoPanel = require("./todo-panel"); +const { handleInitProject, renderSessions } = require("../dom/session-list"); +const { handleManualParse } = require("../dom/observer"); +const { sendToChat } = require("../dom/chat-input"); +const { getProviderByUrl } = require("../../../src/providers"); +const { estimateTokens } = require("../dom/token-estimator"); /** * 渲染窗口列表(浮动管理面板内) */ async function renderWindowList() { - const list = document.getElementById('cuckoo-window-list'); + const list = document.getElementById("cuckoo-window-list"); if (!list) return; try { const res = await window.electronAPI.listProfiles(); @@ -30,54 +48,68 @@ async function renderWindowList() { try { const pvRes = await window.electronAPI.listProviders(); if (pvRes && pvRes.success) { - (pvRes.providers || []).forEach(pv => { providerMap[pv.id] = pv.name; }); + (pvRes.providers || []).forEach((pv) => { + providerMap[pv.id] = pv.name; + }); } } catch (_) {} - list.innerHTML = profiles.map(p => { - const pname = providerMap[p.providerId] || '平台'; - return '
' + - '' + - '' + p.name + '' + + list.innerHTML = profiles + .map((p) => { + const pname = providerMap[p.providerId] || "平台"; + return ( + '
' + + '' + + '' + + p.name + + "" + '|' + - '' + pname + '' + - '' + - '删除' + - '
'; - }).join(''); - list.querySelectorAll('.cuckoo-window-item').forEach(el => { - el.addEventListener('click', async (e) => { + '' + + pname + + "" + + "
" + + '删除' + + "
" + ); + }) + .join(""); + list.querySelectorAll(".cuckoo-window-item").forEach((el) => { + el.addEventListener("click", async (e) => { // 点击删除按钮不触发切换 - if (e.target.classList.contains('cuckoo-window-del')) return; + if (e.target.classList.contains("cuckoo-window-del")) return; const profileId = el.dataset.profileId; try { const r = await window.electronAPI.openProfileWindow(profileId); if (r && r.success) { - showToast(r.focused ? '已切换到该窗口' : '已打开窗口', 2000); + showToast(r.focused ? "已切换到该窗口" : "已打开窗口", 2000); closeWindowManager(); } else { - showToast((r && r.error) || '打开失败', 3000); + showToast((r && r.error) || "打开失败", 3000); } } catch (err) { - showToast('打开窗口失败: ' + (err.message || err), 3000); + showToast("打开窗口失败: " + (err.message || err), 3000); } }); }); // 绑定删除按钮 - list.querySelectorAll('.cuckoo-window-del').forEach(btn => { - btn.addEventListener('click', async (e) => { + list.querySelectorAll(".cuckoo-window-del").forEach((btn) => { + btn.addEventListener("click", async (e) => { e.stopPropagation(); const profileId = btn.dataset.profileId; try { const r = await window.electronAPI.deleteProfileWindow(profileId); if (r && r.success) { - showToast('已删除窗口', 2000); + showToast("已删除窗口", 2000); await renderWindowList(); } else { - showToast((r && r.error) || '删除失败', 3000); + showToast((r && r.error) || "删除失败", 3000); } } catch (err) { - showToast('删除失败: ' + (err.message || err), 3000); + showToast("删除失败: " + (err.message || err), 3000); } }); }); @@ -90,9 +122,9 @@ async function renderWindowList() { * 打开窗口管理浮动面板 */ function openWindowManager() { - const panel = document.getElementById('cuckoo-window-manager'); + const panel = document.getElementById("cuckoo-window-manager"); if (panel) { - panel.classList.remove('cuckoo-hidden'); + panel.classList.remove("cuckoo-hidden"); renderWindowList(); } } @@ -101,17 +133,18 @@ function openWindowManager() { * 关闭窗口管理浮动面板 */ function closeWindowManager() { - const panel = document.getElementById('cuckoo-window-manager'); - if (panel) panel.classList.add('cuckoo-hidden'); + const panel = document.getElementById("cuckoo-window-manager"); + if (panel) panel.classList.add("cuckoo-hidden"); } /** * 生成项目说明文档按钮点击处理 */ function handleGenerateDoc() { - const message = '根据当前项目生成一个项目说明文件,并将文件放到当前项目 .cuckooCode/CUCKOO.md'; - if (!sendToChat(message, '生成文档', 300)) { - showToast('未找到输入框,请确保已打开聊天界面', 3000); + const message = + "根据当前项目生成一个项目说明文件,并将文件放到当前项目 .cuckooCode/CUCKOO.md"; + if (!sendToChat(message, "生成文档", 300)) { + showToast("未找到输入框,请确保已打开聊天界面", 3000); } } @@ -125,7 +158,7 @@ async function loadMcpConfigToJson() { const mcpServers = {}; for (const s of servers) { const def = {}; - if (s.type === 'http') { + if (s.type === "http") { if (s.url) def.url = s.url; if (s.headers) def.headers = s.headers; } else { @@ -135,7 +168,7 @@ async function loadMcpConfigToJson() { } mcpServers[s.name] = def; } - const jsonInput = document.getElementById('cuckoo-mcp-json'); + const jsonInput = document.getElementById("cuckoo-mcp-json"); if (jsonInput) jsonInput.value = JSON.stringify({ mcpServers }, null, 2); } @@ -143,49 +176,66 @@ async function loadMcpConfigToJson() { * 渲染 MCP server 列表 */ async function renderMcpList() { - const list = document.getElementById('cuckoo-mcp-list'); + const list = document.getElementById("cuckoo-mcp-list"); if (!list) return; try { const res = await window.electronAPI.listMcpServers(); const servers = res && res.success ? res.servers : []; if (!servers || servers.length === 0) { - list.innerHTML = '
暂无 MCP Server
'; + list.innerHTML = + '
暂无 MCP Server
'; return; } - list.innerHTML = servers.map(s => { - const status = s.connected ? '已连接' : (s.enabled ? '未连接' : '已禁用'); - const statusColor = s.connected ? '#4ade80' : (s.enabled ? '#ffc107' : '#5d6280'); - return '
' + - '' + s.name + '' + - '' + - '
'; - }).join(''); - - list.querySelectorAll('.cuckoo-mcp-item').forEach(el => { - el.addEventListener('click', async () => { + list.innerHTML = servers + .map((s) => { + const status = s.connected ? "已连接" : s.enabled ? "未连接" : "已禁用"; + const statusColor = s.connected + ? "#4ade80" + : s.enabled + ? "#ffc107" + : "#5d6280"; + return ( + '
' + + '' + + s.name + + "" + + '' + + "
" + ); + }) + .join(""); + + list.querySelectorAll(".cuckoo-mcp-item").forEach((el) => { + el.addEventListener("click", async () => { const name = el.dataset.mcpName; - const server = servers.find(s => s.name === name); + const server = servers.find((s) => s.name === name); if (!server) return; // 点击后立即显示 loading - const dot = el.querySelector('.cuckoo-mcp-dot'); - if (dot) dot.style.background = '#ffc107'; - el.style.pointerEvents = 'none'; + const dot = el.querySelector(".cuckoo-mcp-dot"); + if (dot) dot.style.background = "#ffc107"; + el.style.pointerEvents = "none"; try { if (server.connected || server.enabled) { // 已连接或已启用 → 断开/禁用 await window.electronAPI.disableMcpServer(name); - showToast('已断开 ' + name, 2000); + showToast("已断开 " + name, 2000); } else { // 未启用 → 连接 await window.electronAPI.enableMcpServer(name); - showToast('已连接 ' + name, 2000); + showToast("已连接 " + name, 2000); } await renderMcpList(); await loadMcpConfigToJson(); } catch (err) { - showToast('操作失败: ' + (err.message || err), 3000); + showToast("操作失败: " + (err.message || err), 3000); await renderMcpList(); } }); @@ -199,9 +249,9 @@ async function renderMcpList() { * 打开 MCP 管理面板 */ function openMcpManager() { - const panel = document.getElementById('cuckoo-mcp-manager'); + const panel = document.getElementById("cuckoo-mcp-manager"); if (panel) { - panel.classList.remove('cuckoo-hidden'); + panel.classList.remove("cuckoo-hidden"); renderMcpList(); loadMcpConfigToJson(); } @@ -211,8 +261,8 @@ function openMcpManager() { * 关闭 MCP 管理面板 */ function closeMcpManager() { - const panel = document.getElementById('cuckoo-mcp-manager'); - if (panel) panel.classList.add('cuckoo-hidden'); + const panel = document.getElementById("cuckoo-mcp-manager"); + if (panel) panel.classList.add("cuckoo-hidden"); } /** @@ -221,9 +271,9 @@ function closeMcpManager() { * @returns {string} */ function formatTokenCount(n) { - if (!Number.isFinite(n) || n < 0) return '0'; - if (n >= 1000000) return (n / 1000000).toFixed(2) + 'M'; - if (n >= 1000) return (n / 1000).toFixed(1) + 'K'; + if (!Number.isFinite(n) || n < 0) return "0"; + if (n >= 1000000) return (n / 1000000).toFixed(2) + "M"; + if (n >= 1000) return (n / 1000).toFixed(1) + "K"; return String(Math.round(n)); } @@ -239,7 +289,7 @@ function formatTokenCount(n) { * @returns {string} */ function convTokenStorageKey(sessionId) { - return 'cuckoo-conv-tokens-' + (sessionId || 'default'); + return "cuckoo-conv-tokens-" + (sessionId || "default"); } /** @@ -249,10 +299,12 @@ function convTokenStorageKey(sessionId) { function getCurrentSessionId() { try { const provider = getProviderByUrl(window.location.href); - if (provider && typeof provider.extractSessionId === 'function') { + if (provider && typeof provider.extractSessionId === "function") { return provider.extractSessionId(window.location.href); } - } catch (_) { /* ignore */ } + } catch (_) { + /* ignore */ + } return null; } @@ -262,28 +314,45 @@ function getCurrentSessionId() { * @returns {number} */ function computeAndSaveConversationTokens() { - let text = ''; + let text = ""; try { const provider = getProviderByUrl(window.location.href); - if (provider && typeof provider.getConversationText === 'function') { - text = provider.getConversationText() || ''; + if (provider && typeof provider.getConversationText === "function") { + text = provider.getConversationText() || ""; } - } catch (_) { /* provider 未就绪 */ } + } catch (_) { + /* provider 未就绪 */ + } // Отладка (один раз в 5 сек, чтобы не спамить) try { const now = Date.now(); - if (!computeAndSaveConversationTokens._lastLog || now - computeAndSaveConversationTokens._lastLog > 5000) { + if ( + !computeAndSaveConversationTokens._lastLog || + now - computeAndSaveConversationTokens._lastLog > 5000 + ) { computeAndSaveConversationTokens._lastLog = now; - const cnt = document.querySelectorAll('.ds-message').length; - console.log('[Cookie Code][tokens] .ds-message=' + cnt + ' textLen=' + text.length + ' estTokens=' + estimateTokens(text)); + const cnt = document.querySelectorAll(".ds-message").length; + console.log( + "[Cookie Code][tokens] .ds-message=" + + cnt + + " textLen=" + + text.length + + " estTokens=" + + estimateTokens(text), + ); } } catch (_) {} const tokens = estimateTokens(text); // Запоминаем только если есть что запоминать (> 0) if (tokens > 0) { try { - localStorage.setItem(convTokenStorageKey(getCurrentSessionId()), String(tokens)); - } catch (_) { /* ignore */ } + localStorage.setItem( + convTokenStorageKey(getCurrentSessionId()), + String(tokens), + ); + } catch (_) { + /* ignore */ + } } return tokens; } @@ -294,10 +363,14 @@ function computeAndSaveConversationTokens() { */ function readSavedConversationTokens() { try { - const raw = localStorage.getItem(convTokenStorageKey(getCurrentSessionId())); + const raw = localStorage.getItem( + convTokenStorageKey(getCurrentSessionId()), + ); const n = raw != null ? parseInt(raw, 10) : 0; return Number.isFinite(n) && n > 0 ? n : 0; - } catch (_) { return 0; } + } catch (_) { + return 0; + } } /** @@ -305,7 +378,7 @@ function readSavedConversationTokens() { * @returns {string} */ function sessionKey() { - return getCurrentSessionId() || 'default'; + return getCurrentSessionId() || "default"; } /** @@ -317,7 +390,10 @@ function sessionKey() { function setServerTokensForCurrentSession(total) { if (!Number.isFinite(total) || total <= 0) return; const sid = sessionKey(); - if (!state.serverTokensBySession || typeof state.serverTokensBySession !== 'object') { + if ( + !state.serverTokensBySession || + typeof state.serverTokensBySession !== "object" + ) { state.serverTokensBySession = {}; } const prev = Number(state.serverTokensBySession[sid]) || 0; @@ -332,7 +408,7 @@ function setServerTokensForCurrentSession(total) { function getServerTokensForCurrentSession() { const sid = sessionKey(); const map = state.serverTokensBySession; - const n = map && typeof map === 'object' ? Number(map[sid]) : 0; + const n = map && typeof map === "object" ? Number(map[sid]) : 0; return Number.isFinite(n) && n > 0 ? n : 0; } @@ -343,7 +419,7 @@ function getServerTokensForCurrentSession() { */ function getLocalEstimateForSession(sid) { const m = state.localEstimateBySession; - const n = m && typeof m === 'object' ? Number(m[sid]) : 0; + const n = m && typeof m === "object" ? Number(m[sid]) : 0; return Number.isFinite(n) && n > 0 ? n : 0; } @@ -354,7 +430,10 @@ function getLocalEstimateForSession(sid) { */ function bumpLocalEstimateForSession(sid, value) { if (!Number.isFinite(value) || value <= 0) return; - if (!state.localEstimateBySession || typeof state.localEstimateBySession !== 'object') { + if ( + !state.localEstimateBySession || + typeof state.localEstimateBySession !== "object" + ) { state.localEstimateBySession = {}; } const prev = Number(state.localEstimateBySession[sid]) || 0; @@ -372,7 +451,7 @@ function bumpLocalEstimateForSession(sid, value) { * это давало прыжки 900 → 4K → 1.9K. */ function updateConversationTokenDisplay() { - const countEl = document.getElementById('cuckoo-conv-token-count'); + const countEl = document.getElementById("cuckoo-conv-token-count"); if (!countEl) return; const sid = sessionKey(); @@ -393,11 +472,13 @@ function updateConversationTokenDisplay() { * Слушаем серверные токены из token-interceptor и обновляем state + панель. */ function registerTokenInterceptorListener() { - window.addEventListener('cuckoo:token-update', (e) => { + window.addEventListener("cuckoo:token-update", (e) => { try { const d = e && e.detail ? e.detail : {}; - if (typeof d.total === 'number' && d.total > 0) setServerTokensForCurrentSession(d.total); - if (typeof d.delta === 'number' && d.delta > 0) state.serverTokenDelta = d.delta; + if (typeof d.total === "number" && d.total > 0) + setServerTokensForCurrentSession(d.total); + if (typeof d.delta === "number" && d.delta > 0) + state.serverTokenDelta = d.delta; } catch (_) {} safeUpdateConversationTokenDisplay(); }); @@ -407,7 +488,9 @@ function registerTokenInterceptorListener() { * Безопасный вызов updateConversationTokenDisplay (не роняет слушателя). */ function safeUpdateConversationTokenDisplay() { - try { updateConversationTokenDisplay(); } catch (_) {} + try { + updateConversationTokenDisplay(); + } catch (_) {} } /** @@ -433,88 +516,98 @@ function bindEvents() { // 从 localStorage 恢复延迟配置 try { - const savedMin = localStorage.getItem('cuckoo-send-delay-min'); - const savedMax = localStorage.getItem('cuckoo-send-delay-max'); + const savedMin = localStorage.getItem("cuckoo-send-delay-min"); + const savedMax = localStorage.getItem("cuckoo-send-delay-max"); if (savedMin) state.sendDelayMin = parseInt(savedMin, 10) || 2000; if (savedMax) state.sendDelayMax = parseInt(savedMax, 10) || 4000; - // 同步到输入框 - const minInput = document.getElementById('cuckoo-delay-min'); - const maxInput = document.getElementById('cuckoo-delay-max'); - if (minInput) minInput.value = state.sendDelayMin; - if (maxInput) maxInput.value = state.sendDelayMax; + // 同步到输入框:存储为毫秒,界面显示秒 + const minInput = document.getElementById("cuckoo-delay-min"); + const maxInput = document.getElementById("cuckoo-delay-max"); + if (minInput) minInput.value = state.sendDelayMin / 1000; + if (maxInput) maxInput.value = state.sendDelayMax / 1000; } catch (e) {} - const minimizeBtn = document.getElementById('cuckoo-btn-minimize'); - const initBtn = document.getElementById('cuckoo-btn-init'); - const clearBtn = document.getElementById('cuckoo-btn-clear'); + const minimizeBtn = document.getElementById("cuckoo-btn-minimize"); + const initBtn = document.getElementById("cuckoo-btn-init"); + const clearBtn = document.getElementById("cuckoo-btn-clear"); - minimizeBtn?.addEventListener('click', hideOverlay); - initBtn?.addEventListener('click', handleInitProject); + minimizeBtn?.addEventListener("click", hideOverlay); + initBtn?.addEventListener("click", handleInitProject); // 首次使用提示浮窗:初始化按钮(与右侧初始化项目逻辑一致) - const firstInitBtn = document.getElementById('cuckoo-btn-first-init'); - firstInitBtn?.addEventListener('click', handleInitProject); + const firstInitBtn = document.getElementById("cuckoo-btn-first-init"); + firstInitBtn?.addEventListener("click", handleInitProject); // 首次使用提示浮窗:关闭按钮 - const firstCloseBtn = document.getElementById('cuckoo-btn-first-close'); - firstCloseBtn?.addEventListener('click', hideFirstTimeDialog); - clearBtn?.addEventListener('click', () => { + const firstCloseBtn = document.getElementById("cuckoo-btn-first-close"); + firstCloseBtn?.addEventListener("click", hideFirstTimeDialog); + clearBtn?.addEventListener("click", () => { commandHistory.length = 0; renderHistory(); }); // 手动解析按钮 - const manualParseBtn = document.getElementById('cuckoo-btn-manual-parse'); - manualParseBtn?.addEventListener('click', handleManualParse); + const manualParseBtn = document.getElementById("cuckoo-btn-manual-parse"); + manualParseBtn?.addEventListener("click", handleManualParse); // Экстренная остановка активных дочерних процессов (кнопка Kill в плашке статуса) - const killBtn = document.getElementById('cuckoo-btn-kill'); - killBtn?.addEventListener('click', handleKillProcess); + const killBtn = document.getElementById("cuckoo-btn-kill"); + killBtn?.addEventListener("click", handleKillProcess); // 窗口管理按钮:打开浮动管理面板 - const windowManagerBtn = document.getElementById('cuckoo-btn-window-manager'); - windowManagerBtn?.addEventListener('click', () => { + const windowManagerBtn = document.getElementById("cuckoo-btn-window-manager"); + windowManagerBtn?.addEventListener("click", () => { openWindowManager(); }); // MCP 按钮:打开 MCP 管理面板 - const mcpBtn = document.getElementById('cuckoo-btn-mcp'); - mcpBtn?.addEventListener('click', openMcpManager); + const mcpBtn = document.getElementById("cuckoo-btn-mcp"); + mcpBtn?.addEventListener("click", openMcpManager); // MCP 面板:关闭 - const mcpCloseBtn = document.getElementById('cuckoo-mcp-close'); - mcpCloseBtn?.addEventListener('click', closeMcpManager); + const mcpCloseBtn = document.getElementById("cuckoo-mcp-close"); + mcpCloseBtn?.addEventListener("click", closeMcpManager); // Diff-панель: кнопка в overlay (toggle) - const diffBtn = document.getElementById('cuckoo-btn-diff'); - diffBtn?.addEventListener('click', toggleDiffPanel); + const diffBtn = document.getElementById("cuckoo-btn-diff"); + diffBtn?.addEventListener("click", toggleDiffPanel); // Diff-панель: закрыть - const diffCloseBtn = document.getElementById('cuckoo-diff-close'); - diffCloseBtn?.addEventListener('click', closeDiffPanel); + const diffCloseBtn = document.getElementById("cuckoo-diff-close"); + diffCloseBtn?.addEventListener("click", closeDiffPanel); // Diff-панель: обновить (в зависимости от активной вкладки) - const diffRefreshBtn = document.getElementById('cuckoo-diff-refresh'); - diffRefreshBtn?.addEventListener('click', () => { - const historyTab = document.getElementById('cuckoo-diff-tab-history'); - if (historyTab && historyTab.classList.contains('active')) { - require('./diff-panel').renderGitLog(); + const diffRefreshBtn = document.getElementById("cuckoo-diff-refresh"); + diffRefreshBtn?.addEventListener("click", () => { + const historyTab = document.getElementById("cuckoo-diff-tab-history"); + if (historyTab && historyTab.classList.contains("active")) { + require("./diff-panel").renderGitLog(); } else { renderDiffList(); } }); // Diff-панель: вкладки - document.getElementById('cuckoo-diff-tab-changes')?.addEventListener('click', () => setActiveTab('changes')); - document.getElementById('cuckoo-diff-tab-history')?.addEventListener('click', () => setActiveTab('history')); + document + .getElementById("cuckoo-diff-tab-changes") + ?.addEventListener("click", () => setActiveTab("changes")); + document + .getElementById("cuckoo-diff-tab-history") + ?.addEventListener("click", () => setActiveTab("history")); // Diff-панель: назад к истории / весь коммит - document.getElementById('cuckoo-commit-back')?.addEventListener('click', backToLog); - document.getElementById('cuckoo-commit-full')?.addEventListener('click', openCommitFullDiff); + document + .getElementById("cuckoo-commit-back") + ?.addEventListener("click", backToLog); + document + .getElementById("cuckoo-commit-full") + ?.addEventListener("click", openCommitFullDiff); // Diff-viewer: закрыть - const diffViewerCloseBtn = document.getElementById('cuckoo-diff-viewer-close'); - diffViewerCloseBtn?.addEventListener('click', closeDiffViewer); + const diffViewerCloseBtn = document.getElementById( + "cuckoo-diff-viewer-close", + ); + diffViewerCloseBtn?.addEventListener("click", closeDiffViewer); // Плавающее окошко «Задачи» todoPanel.start(); @@ -523,60 +616,85 @@ function bindEvents() { startTokenCounter(); // MCP 面板:刷新 - const mcpRefreshBtn = document.getElementById('cuckoo-mcp-refresh'); - mcpRefreshBtn?.addEventListener('click', renderMcpList); + const mcpRefreshBtn = document.getElementById("cuckoo-mcp-refresh"); + mcpRefreshBtn?.addEventListener("click", renderMcpList); // MCP 面板:保存配置 - const mcpSaveBtn = document.getElementById('cuckoo-mcp-save'); - mcpSaveBtn?.addEventListener('click', async () => { - const jsonInput = document.getElementById('cuckoo-mcp-json'); + const mcpSaveBtn = document.getElementById("cuckoo-mcp-save"); + mcpSaveBtn?.addEventListener("click", async () => { + const jsonInput = document.getElementById("cuckoo-mcp-json"); if (!jsonInput || !jsonInput.value.trim()) { - showToast('请输入配置', 3000); + showToast("请输入配置", 3000); return; } try { const parsed = JSON.parse(jsonInput.value); - if (!parsed.mcpServers || typeof parsed.mcpServers !== 'object') { - showToast('配置格式错误,需要 mcpServers 对象', 3000); + if (!parsed.mcpServers || typeof parsed.mcpServers !== "object") { + showToast("配置格式错误,需要 mcpServers 对象", 3000); return; } // 校验每个 server 定义是否完整合法(发现错误立即中止,不删旧配置、不覆盖编辑框) for (const [name, def] of Object.entries(parsed.mcpServers)) { - if (!def || typeof def !== 'object' || Array.isArray(def)) { + if (!def || typeof def !== "object" || Array.isArray(def)) { showToast('配置错误:server "' + name + '" 的定义必须是对象', 4000); return; } const hasUrl = def.url !== undefined; const hasCommand = def.command !== undefined; if (hasUrl) { - if (typeof def.url !== 'string' || !def.url.trim()) { - showToast('配置错误:server "' + name + '" 的 url 必须是非空字符串', 4000); + if (typeof def.url !== "string" || !def.url.trim()) { + showToast( + '配置错误:server "' + name + '" 的 url 必须是非空字符串', + 4000, + ); return; } if (hasCommand) { - showToast('配置错误:server "' + name + '" 不能同时指定 url 和 command', 4000); + showToast( + '配置错误:server "' + name + '" 不能同时指定 url 和 command', + 4000, + ); return; } } else if (hasCommand) { - if (typeof def.command !== 'string' || !def.command.trim()) { - showToast('配置错误:server "' + name + '" 的 command 必须是非空字符串', 4000); + if (typeof def.command !== "string" || !def.command.trim()) { + showToast( + '配置错误:server "' + name + '" 的 command 必须是非空字符串', + 4000, + ); return; } } else { - showToast('配置错误:server "' + name + '" 缺少 command 或 url', 4000); + showToast( + '配置错误:server "' + name + '" 缺少 command 或 url', + 4000, + ); return; } if (def.args !== undefined && !Array.isArray(def.args)) { showToast('配置错误:server "' + name + '" 的 args 必须是数组', 4000); return; } - if (def.env !== undefined && (typeof def.env !== 'object' || def.env === null || Array.isArray(def.env))) { + if ( + def.env !== undefined && + (typeof def.env !== "object" || + def.env === null || + Array.isArray(def.env)) + ) { showToast('配置错误:server "' + name + '" 的 env 必须是对象', 4000); return; } - if (def.headers !== undefined && (typeof def.headers !== 'object' || def.headers === null || Array.isArray(def.headers))) { - showToast('配置错误:server "' + name + '" 的 headers 必须是对象', 4000); + if ( + def.headers !== undefined && + (typeof def.headers !== "object" || + def.headers === null || + Array.isArray(def.headers)) + ) { + showToast( + '配置错误:server "' + name + '" 的 headers 必须是对象', + 4000, + ); return; } } @@ -595,112 +713,128 @@ function bindEvents() { for (const [name, def] of Object.entries(parsed.mcpServers)) { const server = { name, - type: def && def.url ? 'http' : 'stdio', + type: def && def.url ? "http" : "stdio", command: def && def.command, - args: def && def.args || [], + args: (def && def.args) || [], url: def && def.url, headers: def && def.headers, env: def && def.env, }; await window.electronAPI.upsertMcpServer(server); } - showToast('配置已保存', 2200); + showToast("配置已保存", 2200); await renderMcpList(); await loadMcpConfigToJson(); // 询问用户是否将 MCP 更新通知发给 AI(不自动发送) try { const confirmed = await showConfirmDialog( - 'MCP 配置已保存。\n\n是否告诉 AI 配置已更新?\n(请确保 AI 当前没有正在进行其他操作)', - { okText: '发送', showCancel: true, cancelText: '取消' } + "MCP 配置已保存。\n\n是否告诉 AI 配置已更新?\n(请确保 AI 当前没有正在进行其他操作)", + { okText: "发送", showCancel: true, cancelText: "取消" }, ); if (!confirmed) return; const res = await window.electronAPI.getMcpTools(); const tools = res && res.success ? res.tools : []; - const serverNames = Array.from(new Set(tools.map(t => t.server))); - let msg = '【MCP 配置已更新】\n\n'; + const serverNames = Array.from(new Set(tools.map((t) => t.server))); + let msg = "【MCP 配置已更新】\n\n"; if (serverNames.length === 0) { - msg += '当前没有已连接的 MCP server。'; + msg += "当前没有已连接的 MCP server。"; } else { - msg += '可用的 MCP server:' + serverNames.join('、') + '。\n'; - msg += '需要时用 mcpListServers() 查看概览,或用 mcpGetTools(serverName) 查看具体工具。'; + msg += "可用的 MCP server:" + serverNames.join("、") + "。\n"; + msg += + "需要时用 mcpListServers() 查看概览,或用 mcpGetTools(serverName) 查看具体工具。"; } - sendToChat(msg, 'MCP信息', 300); + sendToChat(msg, "MCP信息", 300); } catch (err) { - console.error('[Cookie Code] 发送 MCP 信息失败:', err); + console.error("[Cookie Code] 发送 MCP 信息失败:", err); } } catch (err) { - showToast('保存失败: ' + (err.message || err), 3000); + showToast("保存失败: " + (err.message || err), 3000); } }); - - // 浮动面板:新建窗口(всегда DeepSeek, выбор платформы отключён) - const wmNewWindowBtn = document.getElementById('cuckoo-wm-new-window'); - wmNewWindowBtn?.addEventListener('click', async () => { + const wmNewWindowBtn = document.getElementById("cuckoo-wm-new-window"); + wmNewWindowBtn?.addEventListener("click", async () => { try { await window.electronAPI.createProfileWindow(); - showToast('Новое окно DeepSeek создано', 2200); + showToast("Новое окно DeepSeek создано", 2200); await renderWindowList(); } catch (err) { - showToast('Не удалось создать окно: ' + (err.message || err), 3000); + showToast("Не удалось создать окно: " + (err.message || err), 3000); } }); // 浮动面板:关闭 - const wmCloseBtn = document.getElementById('cuckoo-wm-close'); - wmCloseBtn?.addEventListener('click', closeWindowManager); + const wmCloseBtn = document.getElementById("cuckoo-wm-close"); + wmCloseBtn?.addEventListener("click", closeWindowManager); // 浮动面板:刷新列表 - const wmRefreshBtn = document.getElementById('cuckoo-wm-refresh'); - wmRefreshBtn?.addEventListener('click', renderWindowList); + const wmRefreshBtn = document.getElementById("cuckoo-wm-refresh"); + wmRefreshBtn?.addEventListener("click", renderWindowList); // 生成项目说明文档按钮 - const genDocBtn = document.getElementById('cuckoo-btn-gen-doc'); - genDocBtn?.addEventListener('click', handleGenerateDoc); + const genDocBtn = document.getElementById("cuckoo-btn-gen-doc"); + genDocBtn?.addEventListener("click", handleGenerateDoc); // 沉浸式交流按钮 - const immersiveBtn = document.getElementById('cuckoo-btn-immersive'); - immersiveBtn?.addEventListener('click', () => { - const message = '现在你的任何疑问,或没有疑问的选择都需要和我确认 , 确认的方式是 你问一个问题我回答一个问题,然后你再问下一个问题, 最好给我选项, 也要给我个其他的选项, 谢谢 爱你哦'; - if (!sendToChat(message, '沉浸式交流', 300)) { - showToast('未找到输入框,请确保已打开聊天界面', 3000); + const immersiveBtn = document.getElementById("cuckoo-btn-immersive"); + immersiveBtn?.addEventListener("click", () => { + const message = + "现在你的任何疑问,或没有疑问的选择都需要和我确认 , 确认的方式是 你问一个问题我回答一个问题,然后你再问下一个问题, 最好给我选项, 也要给我个其他的选项, 谢谢 爱你哦"; + if (!sendToChat(message, "沉浸式交流", 300)) { + showToast("未找到输入框,请确保已打开聊天界面", 3000); } else { - showToast('已发送沉浸式交流提示', 2200); + showToast("已发送沉浸式交流提示", 2200); } }); // 刷新会话列表按钮 - const refreshSessionsBtn = document.getElementById('cuckoo-btn-refresh-sessions'); - refreshSessionsBtn?.addEventListener('click', renderSessions); + const refreshSessionsBtn = document.getElementById( + "cuckoo-btn-refresh-sessions", + ); + refreshSessionsBtn?.addEventListener("click", renderSessions); // 保存延迟设置按钮 - const saveDelayBtn = document.getElementById('cuckoo-btn-save-delay'); - const delayMinInput = document.getElementById('cuckoo-delay-min'); - const delayMaxInput = document.getElementById('cuckoo-delay-max'); - saveDelayBtn?.addEventListener('click', () => { - const min = parseInt(delayMinInput?.value, 10); - const max = parseInt(delayMaxInput?.value, 10); - if (Number.isNaN(min) || min < 0) { showToast('最小延迟必须是非负整数', 3000); return; } - if (Number.isNaN(max) || max < min) { showToast('最大延迟不能小于最小延迟', 3000); return; } - if (max > 10000) { showToast('最大延迟不能超过 10000ms', 3000); return; } + const saveDelayBtn = document.getElementById("cuckoo-btn-save-delay"); + const delayMinInput = document.getElementById("cuckoo-delay-min"); + const delayMaxInput = document.getElementById("cuckoo-delay-max"); + saveDelayBtn?.addEventListener("click", () => { + // 界面输入为秒,内部/存储统一用毫秒 + const secToMs = (s) => Math.round(parseFloat(s) * 1000); + const min = secToMs(delayMinInput?.value); + const max = secToMs(delayMaxInput?.value); + if (Number.isNaN(min) || min < 0) { + showToast(t("overlay.delay.errMin"), 3000); + return; + } + if (Number.isNaN(max) || max < min) { + showToast(t("overlay.delay.errMax"), 3000); + return; + } + if (max > 10000) { + showToast(t("overlay.delay.errLimit"), 3000); + return; + } state.sendDelayMin = min; state.sendDelayMax = max; - // 保存到 localStorage + // 保存到 localStorage(毫秒) try { - localStorage.setItem('cuckoo-send-delay-min', String(min)); - localStorage.setItem('cuckoo-send-delay-max', String(max)); + localStorage.setItem("cuckoo-send-delay-min", String(min)); + localStorage.setItem("cuckoo-send-delay-max", String(max)); } catch (e) {} - showToast('延迟设置已保存:' + min + ' - ' + max + ' ms', 3000); + showToast( + t("overlay.delay.saved", { min: min / 1000, max: max / 1000 }), + 3000, + ); }); // 悬浮球点击切换面板显隐 - const statusBadge = document.getElementById('cuckoo-status-badge'); - statusBadge?.addEventListener('click', () => { - const overlay = document.getElementById('cuckoo-overlay'); + const statusBadge = document.getElementById("cuckoo-status-badge"); + statusBadge?.addEventListener("click", () => { + const overlay = document.getElementById("cuckoo-overlay"); if (!overlay) return; - if (overlay.classList.contains('cuckoo-hidden')) { + if (overlay.classList.contains("cuckoo-hidden")) { showOverlay(); } else { hideOverlay(); @@ -708,13 +842,13 @@ function bindEvents() { }); // 键盘快捷键 - document.addEventListener('keydown', (e) => { + document.addEventListener("keydown", (e) => { // Ctrl+Shift+C 切换覆盖层显示 - if (e.ctrlKey && e.shiftKey && (e.key === 'C' || e.key === 'c')) { + if (e.ctrlKey && e.shiftKey && (e.key === "C" || e.key === "c")) { e.preventDefault(); - const overlay = document.getElementById('cuckoo-overlay'); + const overlay = document.getElementById("cuckoo-overlay"); if (overlay) { - if (overlay.classList.contains('cuckoo-hidden')) { + if (overlay.classList.contains("cuckoo-hidden")) { showOverlay(); } else { hideOverlay(); @@ -722,7 +856,7 @@ function bindEvents() { } } // Esc 隐藏覆盖层和窗口管理面板 - if (e.key === 'Escape') { + if (e.key === "Escape") { hideOverlay(); closeWindowManager(); closeMcpManager(); diff --git a/src/preload/overlay/template.js b/src/preload/overlay/template.js index 228a960..3150fd8 100644 --- a/src/preload/overlay/template.js +++ b/src/preload/overlay/template.js @@ -108,13 +108,15 @@ function buildOverlayHTML() { t("overlay.label.sendDelay") + "", '
', - '
', - ' ', + '
', + ' ', ' ' + t("overlay.label.delayRange") + "", - ' ', - ' ms', + ' ', + ' ' + + t("overlay.label.delaySeconds") + + "", "
", "
", '