Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ test/coverage.lcov

# Playwright MCP
.playwright-mcp/

# Скачанный бандл DeepSeek (анализ ошибок, не коммитим)
/deepseek-bundle/
/tiktok
/.cuckoo/skills
# Проектная память — личные заметки AI, не коммитим.
Expand Down
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# Changelog

## [5.0.9] - 2026-10-26

### Added

- ⛔ **Уведомление в Telegram при rate-limit DeepSeek (429 / 40029)** — при превышении лимита запросов в Telegram приходит ==КАПС-предупреждение== с просьбой подождать:
- в main-world перехватчике `token-interceptor-inject.js` в патчах `fetch`/`XHR` ловятся HTTP `429` и бизнес-код `40029` (`IP_ACCESS_RESTRICTED`) в теле ответа;
- событие уходит через `postMessage({ __cuckoo: "rate-limit" })`, preload слушает с антиспамом 60с;
- IPC `telegram-rate-limit` → `botsrc.notifyRateLimit()` шлёт сообщение с ожиданием 5–20 минут (учёт `retry-after` при наличии);
- строки локализации `rateLimit.*` (ru/en) в `botsrc/i18n.js`; в окнах-субагентах уведомление подавляется.
- 🔄 **Pull-модель получения initial-prompt в окне субагента** — если push потерялся из-за гонки IPC, окно само запрашивает промпт у main:
- `subagent.js` кэширует combined-промпт по `windowId` и отдаёт через `getInitPrompt()`;
- новый IPC-хендлер `subagent-get-init-prompt` в `src/main/ipc.js`;
- в `chat-input.js` окно-субагент через 20с запрашивает промпт, если не получило пушем.

### Changed

- 🪟 **Отправка initial-prompt в субагент стала устойчивее** — повторные отправки в `3с / 8с / 15с`, абсолютный таймаут до 3 минут (было: 1.2с и 60с) в `subagent.js`.
- ⏳ **Ожидание поля ввода в окне субагента** — увеличено с ~15с до 90с (180 попыток × 500мс), `pendingInitialPrompt` больше не сбрасывается по таймауту (`chat-input.js`).
- 🛡 **Защита от дубля initial-prompt** — `applyInitialPrompt()` с флагом `_initialPromptApplied` не даёт отправить промпт дважды (push + pull).

### Fixed

- 🐛 **Окно субагента больше не остаётся пустым без промпта** — повторные отправки + pull-запрос закрывают гонку долгой загрузки окна.
- 🧹 **DSML/XML-мусор в ответе субагента не считается финальным ответом** — проверка вынесена ==выше анализа cuckoo-блоков== (`subagent-watch.js`):
- детект по `textContent` **и** `innerHTML` (ловит мусор внутри `<pre>` и когда теги съедены markdown-рендером);
- паттерны: маркеры `DSML`, `invoke/parameter/tool_calls name=` с тегом и без;
- при обнаружении в чат уходит предупреждение (на английском) использовать `cuckoo`-блок, ответ не завершается; лимит 3 подсказки + антидубль по снапшоту.

## [5.0.8] - 2026-10-22

### Added
Expand Down
20 changes: 20 additions & 0 deletions botsrc/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,16 @@ const STRINGS = {
// ===== Уведомление об ответе AI =====
"ai.truncated": "…(обрезано, всего {n} симв.)",

// ===== Rate limit (превышение лимита запросов DeepSeek) =====
"rateLimit.title": "⛔️⛔️⛔️ ВНИМАНИЕ! ⛔️⛔️⛔️",
"rateLimit.exceeded": "ТЫ ПРЕВЫСИЛ ЛИМИТ ЗАПРОСОВ К DEEPSEEK!",
"rateLimit.tooOften": "СЛИШКОМ ЧАСТО ОТПРАВЛЯЕШЬ СООБЩЕНИЯ!",
"rateLimit.wait": "⏳ ПОДОЖДИ {min}–{max} МИНУТ, ПОТОМ ПРОБУЙ СНОВА!",
"rateLimit.serverHint": "(Сервер просит подождать минимум ~{retry} мин.)",
"rateLimit.dontSpam":
"Не отправляй сообщения подряд — это только продлит блокировку!",
"rateLimit.statusCode": "Код ответа: {status}",

// ===== Тестовое сообщение =====
"test.send": "👋 Cookie Code: тестовое уведомление. Всё работает.",
},
Expand Down Expand Up @@ -946,6 +956,16 @@ const STRINGS = {
// ===== AI response notification =====
"ai.truncated": "…(truncated, {n} chars total)",

// ===== Rate limit (DeepSeek request limit exceeded) =====
"rateLimit.title": "⛔️⛔️⛔️ WARNING! ⛔️⛔️⛔️",
"rateLimit.exceeded": "YOU HAVE EXCEEDED THE DEEPSEEK REQUEST LIMIT!",
"rateLimit.tooOften": "YOU ARE SENDING MESSAGES TOO OFTEN!",
"rateLimit.wait": "⏳ WAIT {min}–{max} MINUTES, THEN TRY AGAIN!",
"rateLimit.serverHint": "(Server asks to wait at least ~{retry} min.)",
"rateLimit.dontSpam":
"Do not send messages back-to-back — it will only extend the block!",
"rateLimit.statusCode": "Response code: {status}",

// ===== Test message =====
"test.send": "👋 Cookie Code: test notification. All good.",
},
Expand Down
38 changes: 38 additions & 0 deletions botsrc/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5461,6 +5461,43 @@ async function notifyAIResponse(text) {
return telegramBot.sendMessage("🤖 " + body);
}

/**
* Уведомить о превышении rate-limit DeepSeek (429 / IP_ACCESS_RESTRICTED=40029).
* Пишет КАПСОМ, что пользователь превысил лимит и нужно подождать 5-20 минут.
* @param {{status?:number, retryAfterSec?:number}} [info]
*/
async function notifyRateLimit(info) {
const cfg = _read();
if (!cfg.enabled || !cfg.token || !cfg.chatId)
return { success: false, skipped: true };

// Случайная задержка ожидания 5–20 минут.
const minMin = 5;
const maxMin = 20;
const waitMin = minMin + Math.floor(Math.random() * (maxMin - minMin + 1));

// Если сервер прислал retry-after — используем его как ориентир (в минутах).
const retry =
info && Number(info.retryAfterSec) > 0
? Math.ceil(Number(info.retryAfterSec) / 60)
: 0;

const lines = [
_t("rateLimit.title"),
"",
_t("rateLimit.exceeded"),
_t("rateLimit.tooOften"),
"",
_t("rateLimit.wait", { min: waitMin, max: maxMin }),
];
if (retry > 0) lines.push(_t("rateLimit.serverHint", { retry }));
lines.push("", _t("rateLimit.dontSpam"));
if (info && info.status)
lines.push(_t("rateLimit.statusCode", { status: info.status }));

return telegramBot.sendMessage(lines.join("\n"));
}

/** Пингануть бота (getMe) для проверки токена. */
async function ping() {
const cfg = _read();
Expand All @@ -5483,6 +5520,7 @@ module.exports = {
notifyToolResult,
notifyAgentStarted,
notifyAIResponse,
notifyRateLimit,
startTyping,
stopTyping,
notifyAllDone,
Expand Down
30 changes: 30 additions & 0 deletions src/main/ipc.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,22 @@ function registerIpcHandlers() {
}
});

// Pull-модель: дочернее окно само запрашивает initial-prompt, если push
// потерялся из-за гонки IPC (окно долго грузилось и не успело подписаться).
ipcMain.handle("subagent-get-init-prompt", async (event) => {
try {
const ctx = windowState.getContextByWebContents(event.sender);
if (!ctx || !ctx.win || ctx.win.isDestroyed()) {
return { success: false, error: "no window context" };
}
const text = subagent.getInitPrompt(ctx.win.id);
if (!text) return { success: false, error: "no init prompt" };
return { success: true, text };
} catch (err) {
return { success: false, error: err.message };
}
});

// Мост Telegram → окно: ответ на вопрос из TG резолвит тот же promise.
try {
const bot = require("../../botsrc");
Expand Down Expand Up @@ -1450,6 +1466,20 @@ function registerIpcHandlers() {
}
});

// Rate-limit DeepSeek (429 / IP_ACCESS_RESTRICTED) -> КАПС-уведомление в Telegram.
ipcMain.handle("telegram-rate-limit", async (event, info = {}) => {
try {
const ctx = windowState.getContextByWebContents(event.sender);
if (ctx && ctx.isSubagent) {
return { success: false, skipped: true, reason: "subagent window" };
}
const bot = require("../../botsrc");
return await bot.notifyRateLimit(info);
} catch (err) {
return { success: false, error: err.message };
}
});

// Индикатор «ИИ печатает…» в Telegram (пока идёт генерация ответа).
ipcMain.handle("telegram-typing-start", async () => {
try {
Expand Down
34 changes: 28 additions & 6 deletions src/main/subagent.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ function buildAgentPrompt(agentSystemPrompt, task) {
* Дождаться готовности дочернего окна и отправить в него initial-prompt
* (system prompt Cookie Code + промпт агента + task).
*/
// combined initial-prompt по windowId — чтобы окно могло запросить его повторно
// (pull-модель), если push-сообщение потерялось из-за гонки IPC.
const initPrompts = new Map();

/** Получить сохранённый initial-prompt по windowId (или null). */
function getInitPrompt(windowId) {
return initPrompts.get(windowId) || null;
}

function sendInitPromptWhenReady(win, projectDir, extraPrompt, timeoutMs) {
return new Promise((resolve) => {
if (!win || win.isDestroyed()) return resolve(false);
Expand All @@ -92,6 +101,9 @@ function sendInitPromptWhenReady(win, projectDir, extraPrompt, timeoutMs) {
"deepseek",
);
const combined = base + "\n\n" + extraPrompt;
try {
initPrompts.set(win.id, combined);
} catch (_) {}
win.webContents.send("initial-prompt", combined);
sent = true;
console.log(
Expand All @@ -108,15 +120,24 @@ function sendInitPromptWhenReady(win, projectDir, extraPrompt, timeoutMs) {
resolve(false);
}
};
win.webContents.once("did-finish-load", () => setTimeout(send, 1200));
// На случай, если страница уже загрузилась до подписки
setTimeout(send, 3000);
// Абсолютный таймаут
// DeepSeek на холодном старте монтирует React долго — ждём дольше.
win.webContents.once("did-finish-load", () => setTimeout(send, 3000));
// Повторная попытка, если первая не сработала (окно догрузилось позже).
setTimeout(send, 8000);
setTimeout(send, 15000);
// Абсолютный таймаут: ждём до 3 минут, пока окно будет готово.
setTimeout(
() => {
if (!sent) resolve(false);
if (!sent) {
console.warn(
"[Subagent] initial-prompt не отправлен за " +
Math.round(Math.min(timeoutMs, 180000) / 1000) +
"с — окно, возможно, не готово",
);
resolve(false);
}
},
Math.min(timeoutMs, 60000),
Math.min(timeoutMs, 180000),
);
});
}
Expand Down Expand Up @@ -381,4 +402,5 @@ module.exports = {
runAgent,
onSubagentResponse,
hasRunningSubagent,
getInitPrompt,
};
41 changes: 41 additions & 0 deletions src/main/token-interceptor-inject.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,32 @@ function buildTokenInterceptorScript() {
return typeof url === "string" && url.indexOf(COMPLETION) !== -1;
}

// Rate-limit: 429 или бизнес-код 40029 (IP_ACCESS_RESTRICTED).
function publishRateLimit(extra) {
try {
window.postMessage(
{
__cuckoo: "rate-limit",
status: (extra && extra.status) || 0,
retryAfterSec: (extra && extra.retryAfterSec) || 0,
ts: Date.now(),
},
"*",
);
} catch (e) {}
}

// Проверить тело ответа/ошибки на 40029 и вызвать publishRateLimit.
function checkBodyForRateLimit(body) {
if (!body || typeof body !== "string") return;
if (
body.indexOf("40029") === -1 &&
body.indexOf("IP_ACCESS_RESTRICTED") === -1
)
return;
publishRateLimit({ status: 200 });
}

// ---- патч fetch ----
try {
var originalFetch = window.fetch;
Expand All @@ -132,13 +158,26 @@ function buildTokenInterceptorScript() {
var p = originalFetch.apply(this, arguments);
if (!isCompletion(url)) return p;
return p.then(function (response) {
// HTTP 429 — rate limit.
try {
if (response && response.status === 429) {
var ra = 0;
try {
ra =
parseInt(response.headers.get("retry-after") || "0", 10) ||
0;
} catch (e) {}
publishRateLimit({ status: 429, retryAfterSec: ra });
}
} catch (e) {}
try {
var clone = response.clone();
clone
.text()
.then(function (body) {
var u = parseSseText(body);
if (u) publish(u);
checkBodyForRateLimit(body);
})
.catch(function () {});
} catch (e) {}
Expand All @@ -164,8 +203,10 @@ function buildTokenInterceptorScript() {
if (isCompletion(this.__cuckooUrl || "")) {
this.addEventListener("load", function () {
try {
if (self.status === 429) publishRateLimit({ status: 429 });
var u = parseSseText(self.responseText);
if (u) publish(u);
checkBodyForRateLimit(self.responseText);
} catch (e) {}
});
}
Expand Down
2 changes: 2 additions & 0 deletions src/preload/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ let electronAPI = {
telegramTest: () => ipcRenderer.invoke("telegram-test"),
telegramNotifyAI: (text) =>
ipcRenderer.invoke("telegram-notify-ai", { text }),
telegramRateLimit: (info) =>
ipcRenderer.invoke("telegram-rate-limit", info || {}),
telegramTypingStart: () => ipcRenderer.invoke("telegram-typing-start"),
telegramTypingStop: () => ipcRenderer.invoke("telegram-typing-stop"),
telegramWhisperStatus: (opts) =>
Expand Down
Loading
Loading