diff --git a/CHANGELOG.md b/CHANGELOG.md index d01d77c..c969c78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ ### Added - _Нет незарелиженных изменений — всё ниже разнесено по версиям._ +## [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 diff --git a/botsrc/index.js b/botsrc/index.js index a285947..b283439 100644 --- a/botsrc/index.js +++ b/botsrc/index.js @@ -8,12 +8,41 @@ * - notifyToolResult(toolName, ok, detail) → уведомление в TG (diff-стиль для edit). * - входящее сообщение из TG → sendToChat в активном окне DeepSeek. */ -const { telegramBot } = require('./telegram'); +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'); +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'); + } catch (_) { + _logFile = path.join(__dirname, 'telegram-bot.log'); + } + return _logFile; +} + +function _appendLogFile(line) { + try { + const file = _getLogFile(); + try { + if (fs.existsSync(file) && fs.statSync(file).size > LOG_MAX_BYTES) { + fs.renameSync(file, file + '.1'); + } + } catch (_) {} + fs.appendFileSync(file, line + '\n', 'utf-8'); + } catch (_) {} +} + function _read() { const s = settingsStore.readSettings(); return { @@ -28,9 +57,17 @@ function _read() { function _log(level, ...args) { const tag = '[Cookie Code][telegram]'; - if (level === 'error') console.error(tag, ...args); - else if (level === 'warn') console.warn(tag, ...args); - else console.log(tag, ...args); + 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); + } catch (_) {} } /** @@ -68,6 +105,29 @@ async function _sendToChat(text) { } } +/** Выполнить JS в активном окне и вернуть результат. */ +async function _evalInWindow(script) { + const win = windowState.getMainWindow(); + if (!win || win.isDestroyed()) return { success: false, error: 'нет активного окна' }; + try { + const result = await win.webContents.executeJavaScript(script, true); + return { success: true, result }; + } catch (err) { + return { success: false, error: err.message }; + } +} + +/** Получить контекст активного окна (профиль/сессия/projectDir). */ +function _activeContext() { + try { + const win = windowState.getMainWindow(); + if (win && !win.isDestroyed() && win.webContents) { + return windowState.getContextByWebContents(win.webContents); + } + } catch (_) {} + return null; +} + /** Получить id активного окна (для доступа к его todo-списку). */ function _activeSenderId() { try { @@ -597,15 +657,129 @@ async function _handleSettingsCallback(data, cbq) { await telegramBot.answerCallbackQuery(cbq.id); } +/** /stop — прервать задачу и убить активные дочерние процессы. */ +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 (_) {} + } + 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)); + } + return telegramBot.sendMessage('🛑 Остановлено. Убито процессов: ' + count + '.', { parseMode: 'HTML' }); +} + +/** /status — окно, проект, процессы, версия, polling. */ +async function _cmdStatus() { + const cfg = _read(); + const lines = ['📊 Статус Cookie Code', '']; + + 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 ? 'активно' : '❌ нет')); + + 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)); + + let procCount = 0; + try { + const { processManager } = require('../src/main/process-manager'); + procCount = (processManager.activeProcesses && processManager.activeProcesses.size) || 0; + } catch (_) {} + lines.push('⚙️ Активных процессов: ' + procCount + ''); + + let todos = 0, done = 0; + try { + const sid = _activeSenderId(); + if (sid != null) { + const list = require('../src/main/todo-store').getList(sid) || []; + todos = list.length; + done = list.filter((t) => t.status === 'completed').length; + } + } catch (_) {} + 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))); + + 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 inner = res.result || {}; + 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 gitDiff = require('../src/main/git-diff'); + const status = await gitDiff.getStatus(projectDir); + if (!status.success) return telegramBot.sendMessage('⚠️ ' + escapeHtml(status.reason || 'git недоступен')); + const files = status.files || []; + if (files.length === 0) return telegramBot.sendMessage('✅ Изменений нет.'); + + const chunks = []; + let total = 0; + const MAX_TOTAL = 3000; + for (const f of files) { + if (total >= MAX_TOTAL) break; + 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); + 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' }); +} + +/** /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' }); +} + async function _cmdHelp() { const text = [ '🦆 Cookie Code — помощь', '', 'Команды', - '/settings — открыть меню настроек', - '/help — эта справка', - '/todos — список задач активного окна', - '/cancel — отменить ввод значения', + '/settings — открыть меню настроек', + '/status — статус: окно, проект, процессы', + '/stop — прервать задачу и убить процессы', + '/new — новый чат', + '/diff — показать изменения (git diff)', + '/diagnostics — диагностика интеграции', + '/todos — список задач активного окна', + '/cancel — отменить ввод / подтверждение', + '/help — эта справка', '', 'Возможности', '• Уведомления о вызовах инструментов', @@ -668,9 +842,37 @@ async function _handleIncoming(chatId, text) { await _cmdSettings(chatId); return; } + if (cmd === '/stop' || cmd === '/kill') { + await _cmdStop(); + return; + } + if (cmd === '/status' || cmd === '/stat') { + await _cmdStatus(); + return; + } + if (cmd === '/new') { + await _cmdNew(); + return; + } + if (cmd === '/diff') { + await _cmdDiff(); + return; + } + if (cmd === '/diagnostics' || cmd === '/diag') { + await _cmdDiagnostics(); + return; + } if (cmd === '/cancel') { - const had = _settingsWaiting.delete(chatId); - await telegramBot.sendMessage(had ? '✖️ Ввод отменён.' : 'Нечего отменять.'); + const hadSettings = _settingsWaiting.delete(chatId); + // Отменяем все ожидающие approval (AI больше не ждёт подтверждения). + let cancelledApprovals = 0; + for (const id of Array.from(_pendingApprovals.keys())) { + 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('Нечего отменять.'); return; } // Команда /todos — показать список задач активного окна. diff --git a/botsrc/telegram.js b/botsrc/telegram.js index 6ab487f..d21cb27 100644 --- a/botsrc/telegram.js +++ b/botsrc/telegram.js @@ -28,12 +28,24 @@ class TelegramBot { this.onLog = null; // (level, ...args) => void this._lastError = null; this._pollDelayMs = 2000; // пауза при ошибке/пустом ответе + this._queue = []; // очередь исходящих сообщений + this._draining = false; + this._minGapMs = 1100; // ~1 msg/sec на чат (лимит Telegram) + } + + /** Замаскировать токен бота (bot) в произвольной строке. */ + static maskToken(str, token) { + const s = String(str == null ? '' : str); + if (!token) return s; + return s.split(token).join('bot'); } _log(level, ...args) { - if (typeof this.onLog === 'function') { - try { this.onLog(level, ...args); } catch (_) {} - } + if (typeof this.onLog !== 'function') return; + const masked = args.map((a) => + typeof a === 'string' ? TelegramBot.maskToken(a, this.token) : a + ); + try { this.onLog(level, ...masked); } catch (_) {} } /** Настроить токен/chat_id. Возвращает true, если параметры валидны. */ @@ -52,28 +64,125 @@ class TelegramBot { } /** - * Отправить сообщение в Telegram. + * Низкоуровневый вызов Telegram Bot API с retry на 429 (rate limit). + * @param {string} method + * @param {object} body + * @param {{attempts?: number}} [opts] + * @returns {Promise<{success:boolean, status?:number, data?:object, error?:string}>} + */ + async _callApi(method, body, opts = {}) { + 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' }, + 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 + 'с'); + 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: true, status: res.status, data }; + } catch (err) { + if (attempt < attempts - 1) { + await this._sleep(1000 * (attempt + 1)); + continue; + } + return { success: false, error: err.message }; + } + } + return { success: false, error: 'rate limit: превышено число попыток' }; + } + + /** + * Отправить сообщение в Telegram (через очередь, с троттлингом). * @param {string} text * @param {{chatId?: string, parseMode?: string, replyMarkup?: object}} [opts] * @returns {Promise<{success: boolean, error?: string, messageId?: number}>} */ - async sendMessage(text, opts = {}) { - if (!this.token) return { success: false, error: 'token не задан' }; + sendMessage(text, opts = {}) { + if (!this.token) return Promise.resolve({ success: false, error: 'token не задан' }); const chatId = opts.chatId || this.chatId; - if (!chatId) return { success: false, error: 'chat_id не задан' }; + if (!chatId) return Promise.resolve({ success: false, error: 'chat_id не задан' }); - try { - const res = await fetch(this._apiUrl('sendMessage'), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ + return new Promise((resolve) => { + this._queue.push({ + method: 'sendMessage', + body: { chat_id: chatId, text: String(text || ''), parse_mode: opts.parseMode || undefined, disable_web_page_preview: true, reply_markup: opts.replyMarkup || undefined, - }), + }, + resolve, }); + this._drainQueue(); + }); + } + + /** Последовательно разгребает очередь с минимальным интервалом между запросами. */ + async _drainQueue() { + if (this._draining) return; + this._draining = true; + while (this._queue.length > 0) { + const job = this._queue.shift(); + 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 }); + } else { + this._lastError = res.error; + job.resolve({ success: false, error: res.error }); + } + if (this._queue.length > 0) await this._sleep(this._minGapMs); + } + this._draining = false; + } + + /** + * Отправить документ/файл в Telegram. + * @param {string} filePath — путь к файлу на диске + * @param {{chatId?: string, caption?: string, fileName?: string}} [opts] + * @returns {Promise<{success:boolean, messageId?:number, error?:string}>} + */ + async sendDocument(filePath, opts = {}) { + return this._sendFile('sendDocument', 'document', filePath, opts); + } + + /** Отправить фото в Telegram. */ + async sendPhoto(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 не задан' }; + const chatId = opts.chatId || this.chatId; + 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 buf = fs.readFileSync(filePath); + const form = new FormData(); + 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 data = await res.json().catch(() => ({})); if (!res.ok || !data.ok) { const err = (data && data.description) || ('HTTP ' + res.status); @@ -94,25 +203,12 @@ class TelegramBot { * @param {{text?: string, showAlert?: boolean}} [opts] */ async answerCallbackQuery(callbackQueryId, opts = {}) { - if (!this.token) return { success: false, error: 'token не задан' }; - try { - const res = await fetch(this._apiUrl('answerCallbackQuery'), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - callback_query_id: callbackQueryId, - text: opts.text || undefined, - show_alert: !!opts.showAlert, - }), - }); - const data = await res.json().catch(() => ({})); - if (!res.ok || !data.ok) { - return { success: false, error: (data && data.description) || ('HTTP ' + res.status) }; - } - return { success: true }; - } catch (err) { - return { success: false, error: err.message }; - } + const r = await this._callApi('answerCallbackQuery', { + callback_query_id: callbackQueryId, + text: opts.text || undefined, + show_alert: !!opts.showAlert, + }); + return r.success ? { success: true } : { success: false, error: r.error }; } /** @@ -125,27 +221,15 @@ class TelegramBot { if (!this.token) return { success: false, error: 'token не задан' }; const chatId = opts.chatId || this.chatId; if (!chatId) return { success: false, error: 'chat_id не задан' }; - try { - const res = await fetch(this._apiUrl('editMessageText'), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - chat_id: chatId, - message_id: messageId, - text: String(text || ''), - parse_mode: opts.parseMode || undefined, - disable_web_page_preview: true, - reply_markup: opts.replyMarkup || undefined, - }), - }); - const data = await res.json().catch(() => ({})); - if (!res.ok || !data.ok) { - return { success: false, error: (data && data.description) || ('HTTP ' + res.status) }; - } - return { success: true }; - } catch (err) { - return { success: false, error: err.message }; - } + const r = await this._callApi('editMessageText', { + chat_id: chatId, + message_id: messageId, + text: String(text || ''), + parse_mode: opts.parseMode || undefined, + disable_web_page_preview: true, + reply_markup: opts.replyMarkup || undefined, + }); + return r.success ? { success: true } : { success: false, error: r.error }; } /** @@ -263,9 +347,9 @@ class TelegramBot { } _sleep(ms) { - return new Promise((resolve) => { - this.pollTimer = setTimeout(resolve, ms); - }); + // Внимание: не используем this.pollTimer — иначе retry/очередь + // перезапишут таймер long-polling. Обычный setTimeout. + return new Promise((resolve) => setTimeout(resolve, ms)); } getStatus() { diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/assets/photo_1_2026-09-12_19-02-00.jpg b/docs/assets/photo_1_2026-09-12_19-02-00.jpg new file mode 100644 index 0000000..f44b024 Binary files /dev/null and b/docs/assets/photo_1_2026-09-12_19-02-00.jpg differ diff --git a/docs/assets/photo_1_2026-09-12_19-08-16.jpg b/docs/assets/photo_1_2026-09-12_19-08-16.jpg new file mode 100644 index 0000000..b2aab84 Binary files /dev/null and b/docs/assets/photo_1_2026-09-12_19-08-16.jpg differ diff --git a/docs/assets/photo_1_2026-09-13_13-16-57.jpg b/docs/assets/photo_1_2026-09-13_13-16-57.jpg new file mode 100644 index 0000000..1065b23 Binary files /dev/null and b/docs/assets/photo_1_2026-09-13_13-16-57.jpg differ diff --git a/docs/assets/photo_2026-09-13_22-54-33.jpg b/docs/assets/photo_2026-09-13_22-54-33.jpg new file mode 100644 index 0000000..a1b48cf Binary files /dev/null and b/docs/assets/photo_2026-09-13_22-54-33.jpg differ diff --git a/docs/assets/photo_2_2026-09-12_19-02-00.jpg b/docs/assets/photo_2_2026-09-12_19-02-00.jpg new file mode 100644 index 0000000..6e54746 Binary files /dev/null and b/docs/assets/photo_2_2026-09-12_19-02-00.jpg differ diff --git a/docs/assets/photo_2_2026-09-12_19-08-16.jpg b/docs/assets/photo_2_2026-09-12_19-08-16.jpg new file mode 100644 index 0000000..25aff97 Binary files /dev/null and b/docs/assets/photo_2_2026-09-12_19-08-16.jpg differ diff --git a/docs/assets/photo_2_2026-09-13_13-16-57.jpg b/docs/assets/photo_2_2026-09-13_13-16-57.jpg new file mode 100644 index 0000000..05386e2 Binary files /dev/null and b/docs/assets/photo_2_2026-09-13_13-16-57.jpg differ diff --git a/docs/assets/photo_3_2026-09-12_19-02-00.jpg b/docs/assets/photo_3_2026-09-12_19-02-00.jpg new file mode 100644 index 0000000..13e2369 Binary files /dev/null and b/docs/assets/photo_3_2026-09-12_19-02-00.jpg differ diff --git a/docs/assets/photo_4_2026-09-12_19-02-00.jpg b/docs/assets/photo_4_2026-09-12_19-02-00.jpg new file mode 100644 index 0000000..eb09b84 Binary files /dev/null and b/docs/assets/photo_4_2026-09-12_19-02-00.jpg differ diff --git a/docs/assets/photo_5_2026-09-12_19-02-00.jpg b/docs/assets/photo_5_2026-09-12_19-02-00.jpg new file mode 100644 index 0000000..cb3b1a5 Binary files /dev/null and b/docs/assets/photo_5_2026-09-12_19-02-00.jpg differ diff --git a/docs/assets/photo_6_2026-09-12_19-02-00.jpg b/docs/assets/photo_6_2026-09-12_19-02-00.jpg new file mode 100644 index 0000000..76c4bb9 Binary files /dev/null and b/docs/assets/photo_6_2026-09-12_19-02-00.jpg differ diff --git a/docs/en.html b/docs/en.html new file mode 100644 index 0000000..f6d30db --- /dev/null +++ b/docs/en.html @@ -0,0 +1,785 @@ + + + + + +Cookie Code — Desktop AI agent with zero token cost + + + + + + + + +
+ +
+ +
+ + +
+
+

Desktop AI agent
with zero token cost

+

Cookie Code embeds the DeepSeek web chat into a native Electron window, adds a side overlay and turns the chat into a local executor. The AI emits tool calls — they are intercepted, approved, run in a local sandbox and streamed back into the chat. No API key, no token fees.

+ +
+ verified Version 4.0.13 + devices Windows · macOS · Linux + sell MIT License + payments $0 for tokens +
+ +
+ Cookie Code — customized interface with overlay +
+
+
+ + +
+
+
+
psychology
+
+

Why this exists

+
Web chats can think, but they can't act on your machine
+
+
+
    +
  • + savings +
    + Zero token cost + Everything goes through the regular DeepSeek web interface. No API key, no meters. +
    +
  • +
  • + autorenew +
    + Real agent loop + Think → Act → Observe → Repeat. Files, code, shell, DB, MCP — all in one loop. +
    +
  • +
  • + widgets +
    + Native shell + Electron wrapper with its own overlay, themes, glass effect and settings. +
    +
  • +
+
+
+ + +
+
+
+
apps
+
+

Features

+
Everything that makes Cookie Code a full local agent
+
+
+ +
+
+
terminal
+

Tool execution

+

The AI sends tool calls as JavaScript blocks (cuckoo). Each one is intercepted, shown in the overlay and run in a Node sandbox. Failures are highlighted in red.

+
+ +
+
view_agenda
+

Inline tool blocks

+

Every cuckoo block turns into a collapsible card with an icon, the tool name and a file hint taken from the first argument.

+
+ +
+
timer
+

Response meta

+

Every AI reply gets a badge: ⏱ 5.2s · ~380 tok. Real stream time and an approximate token estimate.

+
+ +
+
wallpaper
+

Backgrounds & glass

+

27 hand-picked wallpapers. Full control over blur and opacity of the header, sidebar and tool blocks. Switching is instant.

+
+ +
+
palette
+

RGB username & accents

+

Animated rainbow gradient on the sidebar username. Panel background and button accent colors are configurable via an MD3 palette.

+
+ +
+
send
+

Telegram bot new

+

Control and observe from your phone: tool-call notifications, AI replies, the /todos and /settings commands, incoming messages.

+
+ +
+
extension
+

MCP support

+

Config format compatible with Claude Desktop. Both stdio and http servers, managed from the overlay.

+
+ +
+
auto_fix_high
+

Auto-formatters

+

prettier, biome, gofmt, ruff, rustfmt, shfmt, clang-format — selected by file extension and project config. AI-generated code matches your project style.

+
+ +
+
folder_open
+

Project initialization

+

Pick a directory once — the AI gets a directory tree and a system prompt tailored to your real project. All paths are relative to it.

+
+ +
+
psychology_alt
+

Custom skills

+

Drop a folder into .cuckoo/skills/<name>/ with a SKILL.md and an optional tool.js — it becomes callable via skillList, skillLoad, skillExecute.

+
+ +
+
verified_user
+

Security

+

Tool approval gate (off / risky / all), 30–60 s timeouts, 1 MB output buffer, editable dangerous-command blacklist, paths restricted to the project directory.

+
+ +
+
cloud_sync
+

Session persistence

+

Login, projects and settings live in the app's userData and survive restarts. Tabs are restored automatically.

+
+
+
+
+ + +
+
+
+
handyman
+
+

Available tools

+
Full TypeScript declarations live in tools/cuckoo-tools.d.ts
+
+
+
+ + + + + + + + + + + + + + + + + +
ToolDescription
read, readLinesRead files (with line numbers, offset/limit)
write, editCreate / modify files (auto-format on save)
deleteFileDelete a file
glob, grepFile search (ripgrep-based)
bash, pwshRun commands in cmd.exe / PowerShell
todoWriteStructured task list
webFetchFetch HTTP(S) as Markdown
mysqlSQL queries
mcpCall, mcpListServers, mcpGetToolsMCP server tools
skillList, skillLoad, skillExecuteCustom skills
openBrowserWindow, injectJSElectron browser window + JS injection
+
+
+
+ + +
+
+
+
photo_library
+
+

Screenshots

+
Click any image to open it fullscreen
+
+
+ +
+
+ + +
+
+
+
rocket_launch
+
+

Installation

+
Node.js ≥ 16.0.0 and npm
+
+
+ +
+
+
1
+
+

Clone the repository

+
git clone https://github.com/merfiDEV/Cookie-code.git
+cd Cookie-code
+
+
+
+
2
+
+

Install dependencies

+
npm install
+# If npm blocks postinstall electron (allowScripts), approve it:
+npm install-scripts ls
+npm install-scripts approve electron
+npm install
+
+
+
+
3
+
+

Run or build

+
# Run in dev mode
+npm start
+
+# Build the installer
+npm run build:win             # Windows NSIS
+npm run build:win:portable    # Windows Portable
+npm run build:mac:dmg         # macOS DMG
+
+
+
+
+
+ + +
+
+
+
school
+
+

How to use

+
From launch to your first tool call — four steps
+
+
+ +
+
+
login
+

1. Sign in to DeepSeek

+

The app opens straight into the DeepSeek web chat. Use your regular account — no API key required.

+
+
+
folder_open
+

2. Initialize a project

+

Click “Initialize project” and pick a directory. The AI gets the directory tree and a system prompt.

+
+
+
chat
+

3. Chat with the AI

+

Ask it to edit files, run commands, search code. Everything runs locally in a sandbox.

+
+
+
sync
+

4. Watch the loop

+

Results are sent back to the AI automatically, and the loop continues until the task is done.

+
+
+
+
+ + +
+
+
+
tune
+
+

Configuration

+
All settings live in cuckoo-settings.json inside the app userData
+
+
+
// cuckoo-settings.json
+{
+  "background": "miku",
+  "backgroundBlur": 0,
+  "headerBlur": 12,
+  "sidebarBlur": 12,
+  "headerOpacity": 45,
+  "sidebarOpacity": 45,
+  "toolBlockOpacity": 55,
+  "toolBlockBlur": 0,
+  "rgbUsername": true,
+  "formattersEnabled": true,
+  "fileChipEnabled": true,
+  "showProducedFiles": true,
+  "language": "ru",
+  "telegramEnabled": false,
+  "telegramBotToken": "",
+  "telegramChatId": "",
+  "telegramNotifyTools": false,
+  "telegramChatFeed": false
+}
+
+
+ + +
+
+

Ready to try Cookie Code?

+

Free, open source and with zero tokens spent. Just download the release and run it.

+ +
+
+ +
+
+
+ © Cookie Code Contributors · GitHub · MIT License +
+
+
+ +
+ + + + + + + + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..f44bef6 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,790 @@ + + + + + +Cookie Code — AI-агент с нулевой стоимостью токенов + + + + + + + + +
+ +
+ +
+ + +
+
+

AI-агент на рабочем столе
с нулевой стоимостью токенов

+

+ Cookie Code встраивает веб-чат DeepSeek в нативное окно Electron, добавляет боковой оверлей + и превращает чат в локальный исполнитель. AI генерирует вызовы инструментов — они перехватываются, + подтверждаются, выполняются в локальной песочнице и стримятся обратно в чат. + Никакого API-ключа, никакой оплаты за токены. +

+ +
+ verified Версия 4.0.13 + devices Windows · macOS · Linux + sell Лицензия MIT + payments 0 ₽ за токены +
+ +
+ Cookie Code — кастомизированный интерфейс с оверлеем +
+
+
+ + +
+
+
+
psychology
+
+

Зачем это нужно

+
Веб-чаты умеют думать, но не действовать на вашей машине
+
+
+
    +
  • + savings +
    + Нулевая стоимость токенов + Всё идёт через обычный веб-интерфейс DeepSeek. Никакого API-ключа, никаких счётчиков. +
    +
  • +
  • + autorenew +
    + Настоящий агентный цикл + Думай → Действуй → Наблюдай → Повторяй. Файлы, код, shell, БД, MCP — всё в одном цикле. +
    +
  • +
  • + widgets +
    + Нативная оболочка + Electron-обёртка с собственным оверлеем, темами, стеклянным эффектом и настройками. +
    +
  • +
+
+
+ + +
+
+
+
apps
+
+

Возможности

+
Всё, что делает Cookie Code полноценным локальным агентом
+
+
+ +
+
+
terminal
+

Выполнение инструментов

+

AI отправляет вызовы как блоки JavaScript (cuckoo). Каждый перехватывается, показывается в оверлее и выполняется в песочнице Node. Ошибки подсвечиваются красным.

+
+ +
+
view_agenda
+

Инлайн tool-блоки

+

Каждый cuckoo-блок превращается в раскрывающуюся карточку с иконкой, названием инструмента и подсказкой файла из первого аргумента.

+
+ +
+
timer
+

Мета ответа

+

Под каждым ответом AI — бейдж ⏱ 5.2s · ~380 tok. Реальное время стрима и приблизительная оценка токенов.

+
+ +
+
wallpaper
+

Фоны и стекло

+

27 вручную отобранных обоев. Полный контроль над размытием и прозрачностью шапки, сайдбара и tool-блоков. Смена — мгновенная.

+
+ +
+
palette
+

RGB-ник и акценты

+

Анимированный радужный градиент ника в сайдбаре. Настройка акцентного цвета кнопок панели и её фона через палитру MD3.

+
+ +
+
send
+

Telegram-бот new

+

Управление и наблюдение с телефона: уведомления о вызовах, ответы AI, команды /todos, /settings, входящие сообщения.

+
+ +
+
extension
+

Поддержка MCP

+

Формат конфигурации, совместимый с Claude Desktop. stdio и http-серверы, управление из оверлея.

+
+ +
+
auto_fix_high
+

Авто-форматтеры

+

prettier, biome, gofmt, ruff, rustfmt, shfmt, clang-format — по расширению и конфигу проекта. Код AI приводится к стилю вашего проекта.

+
+ +
+
folder_open
+

Инициализация проекта

+

Выберите каталог один раз — AI получает дерево и системный промпт, адаптированный к реальному проекту. Все пути — относительно него.

+
+ +
+
psychology_alt
+

Пользовательские скиллы

+

Положите папку в .cuckoo/skills/<name>/ с SKILL.md и опциональным tool.js — и она вызывается через skillList, skillLoad, skillExecute.

+
+ +
+
verified_user
+

Безопасность

+

Гейт подтверждения вызовов (off / risky / all), таймауты 30–60 с, буфер 1 МБ, редактируемый блэклист опасных команд, ограничение путей каталогом проекта.

+
+ +
+
cloud_sync
+

Персистентность сессии

+

Логин, проекты и настройки хранятся в userData приложения и переживают перезапуск. Восстановление вкладок — автоматическое.

+
+
+
+
+ + +
+
+
+
handyman
+
+

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

+
Полные TypeScript-декларации — в tools/cuckoo-tools.d.ts
+
+
+
+ + + + + + + + + + + + + + + + + +
ИнструментОписание
read, readLinesЧтение файлов (с номерами строк, offset/limit)
write, editСоздание / изменение файлов (авто-форматирование при сохранении)
deleteFileУдаление файла
glob, grepПоиск по файлам (на базе ripgrep)
bash, pwshВыполнение команд в cmd.exe / PowerShell
todoWriteСтруктурированный список задач
webFetchЗагрузка HTTP(S) как Markdown
mysqlSQL-запросы
mcpCall, mcpListServers, mcpGetToolsИнструменты MCP-серверов
skillList, skillLoad, skillExecuteПользовательские скиллы
openBrowserWindow, injectJSОкно браузера Electron + инъекция JS
+
+
+
+ + +
+
+
+
photo_library
+
+

Скриншоты

+
Кликните по изображению, чтобы открыть во весь экран
+
+
+ +
+
+ + +
+
+
+
rocket_launch
+
+

Установка

+
Node.js ≥ 16.0.0 и npm
+
+
+ +
+
+
1
+
+

Клонировать репозиторий

+
git clone https://github.com/merfiDEV/Cookie-code.git
+cd Cookie-code
+
+
+
+
2
+
+

Установить зависимости

+
npm install
+# Если npm блокирует postinstall electron (allowScripts), одобрить:
+npm install-scripts ls
+npm install-scripts approve electron
+npm install
+
+
+
+
3
+
+

Запустить или собрать

+
# Запустить в режиме разработки
+npm start
+
+# Собрать установщик
+npm run build:win             # Windows NSIS
+npm run build:win:portable    # Windows Portable
+npm run build:mac:dmg         # macOS DMG
+
+
+
+
+
+ + +
+
+
+
school
+
+

Как пользоваться

+
От запуска до первого вызова инструмента — четыре шага
+
+
+ +
+
+
login
+

1. Войдите в DeepSeek

+

Приложение сразу открывается в веб-чате DeepSeek. Используйте свой обычный аккаунт — никакой API-ключ не нужен.

+
+
+
folder_open
+

2. Инициализируйте проект

+

Нажмите «Инициализировать проект» и выберите каталог. AI получает дерево каталога и системный промпт.

+
+
+
chat
+

3. Общайтесь с AI

+

Просите отредактировать файлы, выполнить команды, найти что-то в коде. Всё выполняется локально в песочнице.

+
+
+
sync
+

4. Наблюдайте цикл

+

Результаты автоматически отправляются обратно AI, и цикл продолжается, пока задача не будет решена.

+
+
+
+
+ + +
+
+
+
tune
+
+

Конфигурация

+
Все настройки — в cuckoo-settings.json в userData приложения
+
+
+
// cuckoo-settings.json
+{
+  "background": "miku",
+  "backgroundBlur": 0,
+  "headerBlur": 12,
+  "sidebarBlur": 12,
+  "headerOpacity": 45,
+  "sidebarOpacity": 45,
+  "toolBlockOpacity": 55,
+  "toolBlockBlur": 0,
+  "rgbUsername": true,
+  "formattersEnabled": true,
+  "fileChipEnabled": true,
+  "showProducedFiles": true,
+  "language": "ru",
+  "telegramEnabled": false,
+  "telegramBotToken": "",
+  "telegramChatId": "",
+  "telegramNotifyTools": false,
+  "telegramChatFeed": false
+}
+
+
+ + +
+
+

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

+

Бесплатно, с открытым исходным кодом и без единого токена. Просто скачайте релиз и запустите.

+ +
+
+ +
+
+
+ © Cookie Code Contributors · GitHub · Лицензия MIT +
+
+
+ +
+ + + + + + + + diff --git a/package-lock.json b/package-lock.json index afc81d3..e377921 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cuckoo-code", - "version": "4.0.19", + "version": "4.0.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cuckoo-code", - "version": "4.0.19", + "version": "4.0.20", "license": "MIT", "os": [ "win32", diff --git a/package.json b/package.json index 3de6450..ca41188 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cuckoo-code", - "version": "4.0.19", + "version": "4.0.20", "description": "DeepSeek 桌面应用——内嵌浏览器,自动识别 AI 回复中的命令和工具调用并执行", "main": "main.js", "scripts": { @@ -45,7 +45,8 @@ "homepage": "https://github.com/merfiDEV/Cookie-code#readme", "devDependencies": { "electron": "^33.0.0", - "electron-builder": "^24.13.3" + "electron-builder": "^24.13.3", + "prettier": "^3.3.3" }, "engines": { "node": ">=16.0.0" diff --git a/src/main/ipc.js b/src/main/ipc.js index 81d9317..883f480 100644 --- a/src/main/ipc.js +++ b/src/main/ipc.js @@ -417,6 +417,26 @@ function registerIpcHandlers() { } }); + // Открыть новый чат (переход на домашнюю страницу провайдера). + ipcMain.handle('new-chat', async (event) => { + const ctx = windowState.getContextByWebContents(event.sender); + const win = ctx ? ctx.win : null; + if (!win || win.isDestroyed()) return { success: false, error: '窗口已关闭' }; + let url = null; + try { + const { getProviderByUrl } = require('../providers'); + const provider = getProviderByUrl(win.webContents.getURL()); + if (provider && provider.homeUrl) url = provider.homeUrl; + } catch (_) {} + if (!url) url = 'https://chat.deepseek.com/'; + try { + await win.webContents.loadURL(url); + return { success: true }; + } catch (err) { + return { success: false, error: err.message }; + } + }); + // 执行命令 ipcMain.handle('execute-command', async (event, { command, id }) => { if (!command || typeof command !== 'string') { diff --git a/src/preload/api.js b/src/preload/api.js index e57ecb5..22aef9a 100644 --- a/src/preload/api.js +++ b/src/preload/api.js @@ -54,6 +54,9 @@ let electronAPI = { navigateSession: (sessionId) => { return ipcRenderer.invoke('navigate-session', { sessionId }); }, + newChat: () => { + return ipcRenderer.invoke('new-chat'); + }, createProfileWindow: () => { return ipcRenderer.invoke('create-profile-window'); }, @@ -192,6 +195,12 @@ let electronAPI = { openChangelogFile: () => { return ipcRenderer.invoke('whats-new-open-changelog'); }, + // ========== Диагностика интеграции (выполняется в контексте страницы) ========== + runDiagnosticsText: async () => { + const { runDiagnostics, formatReportText } = require('./dom/diagnostics'); + const report = await runDiagnostics(); + return formatReportText(report); + }, }; try {