@link-assistant/hive-mind 1.69.11 → 1.69.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/limits-i18n.lib.mjs +145 -0
- package/src/limits.lib.mjs +46 -74
- package/src/locales/en.lino +52 -0
- package/src/locales/hi.lino +52 -0
- package/src/locales/ru.lino +53 -1
- package/src/locales/zh.lino +52 -0
- package/src/solve.results.lib.mjs +24 -0
- package/src/telegram-bot.mjs +9 -8
- package/src/telegram-safe-reply.lib.mjs +117 -14
- package/src/telegram-solve-queue-command.lib.mjs +18 -6
- package/src/telegram-solve-queue.helpers.lib.mjs +58 -6
- package/src/telegram-solve-queue.lib.mjs +65 -41
- package/src/version-info.lib.mjs +52 -15
package/src/locales/ru.lino
CHANGED
|
@@ -80,6 +80,10 @@ ru
|
|
|
80
80
|
limits.current_week_sonnet_only "Текущая неделя (только Sonnet)"
|
|
81
81
|
limits.disabled_by_admin "`--show-limits` отключён администратором бота."
|
|
82
82
|
limits.disk_space "Дисковое пространство"
|
|
83
|
+
limits.duration_day_short "д"
|
|
84
|
+
limits.duration_hour_short "ч"
|
|
85
|
+
limits.duration_minute_short "мин"
|
|
86
|
+
limits.duration_second_short "с"
|
|
83
87
|
limits.end "Конец"
|
|
84
88
|
limits.five_hour_session "5-часовой сеанс"
|
|
85
89
|
limits.five_min_load_avg "средняя нагрузка за 5 мин"
|
|
@@ -92,9 +96,41 @@ ru
|
|
|
92
96
|
limits.passed "прошло"
|
|
93
97
|
limits.plan "План"
|
|
94
98
|
limits.ram "RAM"
|
|
99
|
+
limits.reason_claude_5_hour_session "Лимит 5-часового сеанса Claude {{currentPercent}}% (порог: {{thresholdPercent}})"
|
|
100
|
+
limits.reason_claude_running "Процесс Claude уже выполняется"
|
|
101
|
+
limits.reason_claude_weekly "Недельный лимит Claude {{currentPercent}}% (порог: {{thresholdPercent}})"
|
|
102
|
+
limits.reason_codex_5_hour_session "Лимит 5-часового сеанса Codex {{currentPercent}}% (порог: {{thresholdPercent}})"
|
|
103
|
+
limits.reason_codex_running "Процесс Codex уже выполняется"
|
|
104
|
+
limits.reason_codex_weekly "Недельный лимит Codex {{currentPercent}}% (порог: {{thresholdPercent}})"
|
|
105
|
+
limits.reason_cpu_usage "Использование CPU {{currentPercent}}% (порог: {{thresholdPercent}})"
|
|
106
|
+
limits.reason_disk_usage "Использование диска {{currentPercent}}% (порог: {{thresholdPercent}})"
|
|
107
|
+
limits.reason_gemini_running "Процесс Gemini CLI уже выполняется"
|
|
108
|
+
limits.reason_github_api "Использование GitHub API {{currentPercent}}% (порог: {{thresholdPercent}})"
|
|
109
|
+
limits.reason_min_interval "Минимальный интервал между командами ещё не прошёл"
|
|
110
|
+
limits.reason_qwen_running "Процесс Qwen Code уже выполняется"
|
|
111
|
+
limits.reason_ram_usage "Использование RAM {{currentPercent}}% (порог: {{thresholdPercent}})"
|
|
112
|
+
limits.reason_threshold_exceeded "Превышен порог {{metric}}"
|
|
113
|
+
limits.remaining "осталось {{duration}}"
|
|
95
114
|
limits.requests "запросов"
|
|
96
115
|
limits.resets_at "Сброс {{time}}"
|
|
97
116
|
limits.resets_in "Сброс через {{duration}}"
|
|
117
|
+
limits.resource_limit_exceeded "Превышен лимит ресурсов"
|
|
118
|
+
limits.solve_queue_status "Статус очереди solve"
|
|
119
|
+
limits.queue_completed "Завершено"
|
|
120
|
+
limits.queue_failed "С ошибкой"
|
|
121
|
+
limits.queue_and_more "и ещё {{count}}"
|
|
122
|
+
limits.queue_pending "ожидает"
|
|
123
|
+
limits.queue_processing "выполняется"
|
|
124
|
+
limits.queue_processes "процессов: {{count}}"
|
|
125
|
+
limits.queue_status_cancelled "отменено"
|
|
126
|
+
limits.queue_status_failed "ошибка"
|
|
127
|
+
limits.queue_status_queued "в очереди"
|
|
128
|
+
limits.queue_status_started "запущено"
|
|
129
|
+
limits.queue_status_starting "запускается"
|
|
130
|
+
limits.queue_status_waiting "ожидание"
|
|
131
|
+
limits.queue_waiting_current_command "ожидание текущей команды"
|
|
132
|
+
limits.queue_waiting_in_queue "Ожидание в очереди"
|
|
133
|
+
limits.queues "Очереди"
|
|
98
134
|
limits.seven_day_all_models "7 дней, все модели"
|
|
99
135
|
limits.seven_day_sonnet_only "7 дней, только Sonnet"
|
|
100
136
|
limits.session "сеанс"
|
|
@@ -107,10 +143,12 @@ ru
|
|
|
107
143
|
telegram.fetching_limits "🔄 Получение лимитов использования..."
|
|
108
144
|
telegram.gathering_version "🔄 Сбор информации о версии..."
|
|
109
145
|
telegram.usage_limits_title "📊 *Лимиты использования*"
|
|
110
|
-
telegram.version_information_title "🤖 *Информация о
|
|
146
|
+
telegram.version_information_title "🤖 *Информация о версиях*"
|
|
147
|
+
telegram.formatting_fallback_warning "⚠️ Обнаружена ошибка форматирования. Показываю обычный текст."
|
|
111
148
|
telegram.limits_only_in_groups "❌ Команда /limits работает только в групповых чатах. Добавьте бота в группу и сделайте его администратором."
|
|
112
149
|
telegram.version_only_in_groups "❌ Команда /version работает только в групповых чатах. Добавьте бота в группу и сделайте его администратором."
|
|
113
150
|
telegram.solve_only_in_groups "❌ Команда {{commandDisplay}} работает только в групповых чатах. Добавьте бота в группу и сделайте его администратором."
|
|
151
|
+
telegram.solve_queue_only_in_groups "❌ Команда /solve_queue работает только в групповых чатах. Добавьте бота в группу и сделайте его администратором."
|
|
114
152
|
telegram.hive_only_in_groups "❌ Команда /hive работает только в групповых чатах. Добавьте бота в группу и сделайте его администратором."
|
|
115
153
|
telegram.solve_disabled "❌ Команда solve отключена в этом экземпляре бота."
|
|
116
154
|
telegram.hive_disabled "❌ Команда /hive отключена в этом экземпляре бота."
|
|
@@ -215,6 +253,20 @@ ru
|
|
|
215
253
|
language.ru "Русский"
|
|
216
254
|
language.zh "Китайский"
|
|
217
255
|
language.hi "Хинди"
|
|
256
|
+
version.ai_agents "AI-агенты"
|
|
257
|
+
version.browsers "Браузеры"
|
|
258
|
+
version.browser_automation "Автоматизация браузера"
|
|
259
|
+
version.connected "подключено"
|
|
260
|
+
version.development_tools "Инструменты разработки"
|
|
261
|
+
version.environment "Среда"
|
|
262
|
+
version.architecture "Архитектура"
|
|
263
|
+
version.kernel "Ядро"
|
|
264
|
+
version.not_connected "не подключено"
|
|
265
|
+
version.os "ОС"
|
|
266
|
+
version.platform "Платформа"
|
|
267
|
+
version.process_running_restart_needed "Запущенный процесс: `{{processVersion}}` (требуется перезапуск)"
|
|
268
|
+
version.system "Система"
|
|
269
|
+
version.version "Версия"
|
|
218
270
|
prompt.user.issue_to_solve "Задача для решения: {{issueUrl}}"
|
|
219
271
|
prompt.user.issue_linked_to_pr "Задача для решения: задача, связанная с PR #{{prNumber}}"
|
|
220
272
|
prompt.user.prepared_branch "Подготовленная ветка: {{branchName}}"
|
package/src/locales/zh.lino
CHANGED
|
@@ -80,6 +80,10 @@ zh
|
|
|
80
80
|
limits.current_week_sonnet_only "本周(仅 Sonnet)"
|
|
81
81
|
limits.disabled_by_admin "`--show-limits` 已被机器人管理员禁用。"
|
|
82
82
|
limits.disk_space "磁盘空间"
|
|
83
|
+
limits.duration_day_short "天"
|
|
84
|
+
limits.duration_hour_short "小时"
|
|
85
|
+
limits.duration_minute_short "分钟"
|
|
86
|
+
limits.duration_second_short "秒"
|
|
83
87
|
limits.end "结束"
|
|
84
88
|
limits.five_hour_session "5 小时会话"
|
|
85
89
|
limits.five_min_load_avg "5 分钟平均负载"
|
|
@@ -92,9 +96,41 @@ zh
|
|
|
92
96
|
limits.passed "已过"
|
|
93
97
|
limits.plan "方案"
|
|
94
98
|
limits.ram "内存"
|
|
99
|
+
limits.reason_claude_5_hour_session "Claude 5 小时会话限额为 {{currentPercent}}%(阈值:{{thresholdPercent}})"
|
|
100
|
+
limits.reason_claude_running "Claude 进程已在运行"
|
|
101
|
+
limits.reason_claude_weekly "Claude 每周限额为 {{currentPercent}}%(阈值:{{thresholdPercent}})"
|
|
102
|
+
limits.reason_codex_5_hour_session "Codex 5 小时会话限额为 {{currentPercent}}%(阈值:{{thresholdPercent}})"
|
|
103
|
+
limits.reason_codex_running "Codex 进程已在运行"
|
|
104
|
+
limits.reason_codex_weekly "Codex 每周限额为 {{currentPercent}}%(阈值:{{thresholdPercent}})"
|
|
105
|
+
limits.reason_cpu_usage "CPU 使用率为 {{currentPercent}}%(阈值:{{thresholdPercent}})"
|
|
106
|
+
limits.reason_disk_usage "磁盘使用率为 {{currentPercent}}%(阈值:{{thresholdPercent}})"
|
|
107
|
+
limits.reason_gemini_running "Gemini CLI 进程已在运行"
|
|
108
|
+
limits.reason_github_api "GitHub API 使用率为 {{currentPercent}}%(阈值:{{thresholdPercent}})"
|
|
109
|
+
limits.reason_min_interval "尚未达到命令之间的最小间隔"
|
|
110
|
+
limits.reason_qwen_running "Qwen Code 进程已在运行"
|
|
111
|
+
limits.reason_ram_usage "内存使用率为 {{currentPercent}}%(阈值:{{thresholdPercent}})"
|
|
112
|
+
limits.reason_threshold_exceeded "{{metric}} 超过阈值"
|
|
113
|
+
limits.remaining "剩余 {{duration}}"
|
|
95
114
|
limits.requests "请求"
|
|
96
115
|
limits.resets_at "{{time}} 重置"
|
|
97
116
|
limits.resets_in "{{duration}} 后重置"
|
|
117
|
+
limits.resource_limit_exceeded "资源限额已超出"
|
|
118
|
+
limits.solve_queue_status "Solve 队列状态"
|
|
119
|
+
limits.queue_completed "已完成"
|
|
120
|
+
limits.queue_failed "失败"
|
|
121
|
+
limits.queue_and_more "还有 {{count}} 个"
|
|
122
|
+
limits.queue_pending "等待"
|
|
123
|
+
limits.queue_processing "处理中"
|
|
124
|
+
limits.queue_processes "{{count}} 个进程"
|
|
125
|
+
limits.queue_status_cancelled "已取消"
|
|
126
|
+
limits.queue_status_failed "失败"
|
|
127
|
+
limits.queue_status_queued "已排队"
|
|
128
|
+
limits.queue_status_started "已启动"
|
|
129
|
+
limits.queue_status_starting "正在启动"
|
|
130
|
+
limits.queue_status_waiting "等待中"
|
|
131
|
+
limits.queue_waiting_current_command "等待当前命令"
|
|
132
|
+
limits.queue_waiting_in_queue "在队列中等待"
|
|
133
|
+
limits.queues "队列"
|
|
98
134
|
limits.seven_day_all_models "7 天所有模型"
|
|
99
135
|
limits.seven_day_sonnet_only "7 天仅 Sonnet"
|
|
100
136
|
limits.session "会话"
|
|
@@ -108,9 +144,11 @@ zh
|
|
|
108
144
|
telegram.gathering_version "🔄 正在收集版本信息……"
|
|
109
145
|
telegram.usage_limits_title "📊 *使用限额*"
|
|
110
146
|
telegram.version_information_title "🤖 *版本信息*"
|
|
147
|
+
telegram.formatting_fallback_warning "⚠️ 检测到格式错误。正在显示纯文本备用内容。"
|
|
111
148
|
telegram.limits_only_in_groups "❌ /limits 命令仅在群聊中有效。请将本机器人加入群组并设为管理员。"
|
|
112
149
|
telegram.version_only_in_groups "❌ /version 命令仅在群聊中有效。请将本机器人加入群组并设为管理员。"
|
|
113
150
|
telegram.solve_only_in_groups "❌ {{commandDisplay}} 命令仅在群聊中有效。请将本机器人加入群组并设为管理员。"
|
|
151
|
+
telegram.solve_queue_only_in_groups "❌ /solve_queue 命令仅在群聊中有效。请将本机器人加入群组并设为管理员。"
|
|
114
152
|
telegram.hive_only_in_groups "❌ /hive 命令仅在群聊中有效。请将本机器人加入群组并设为管理员。"
|
|
115
153
|
telegram.solve_disabled "❌ 此机器人实例已禁用 solve 命令。"
|
|
116
154
|
telegram.hive_disabled "❌ 此机器人实例已禁用 /hive 命令。"
|
|
@@ -215,6 +253,20 @@ zh
|
|
|
215
253
|
language.ru "俄语"
|
|
216
254
|
language.zh "中文"
|
|
217
255
|
language.hi "印地语"
|
|
256
|
+
version.ai_agents "AI 代理"
|
|
257
|
+
version.browsers "浏览器"
|
|
258
|
+
version.browser_automation "浏览器自动化"
|
|
259
|
+
version.connected "已连接"
|
|
260
|
+
version.development_tools "开发工具"
|
|
261
|
+
version.environment "环境"
|
|
262
|
+
version.architecture "架构"
|
|
263
|
+
version.kernel "内核"
|
|
264
|
+
version.not_connected "未连接"
|
|
265
|
+
version.os "操作系统"
|
|
266
|
+
version.platform "平台"
|
|
267
|
+
version.process_running_restart_needed "正在运行的进程:`{{processVersion}}`(需要重启)"
|
|
268
|
+
version.system "系统"
|
|
269
|
+
version.version "版本"
|
|
218
270
|
prompt.user.issue_to_solve "要解决的问题:{{issueUrl}}"
|
|
219
271
|
prompt.user.issue_linked_to_pr "要解决的问题:与 PR #{{prNumber}} 关联的问题"
|
|
220
272
|
prompt.user.prepared_branch "为你准备的分支:{{branchName}}"
|
|
@@ -361,6 +361,20 @@ const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
|
|
|
361
361
|
}
|
|
362
362
|
};
|
|
363
363
|
|
|
364
|
+
const wasFileTouchedAfterCommit = async (tempDir, commitHash, fileName) => {
|
|
365
|
+
const changedCommitsResult = await $({ cwd: tempDir, silent: true })`git log --format=%H ${commitHash}..HEAD -- ${fileName}`;
|
|
366
|
+
if (changedCommitsResult.code === 0) {
|
|
367
|
+
return Boolean(changedCommitsResult.stdout?.trim());
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (changedCommitsResult.code !== 0) {
|
|
371
|
+
await log(` Could not inspect ${fileName} changes after initial commit`, { verbose: true });
|
|
372
|
+
await log(` git log output: ${changedCommitsResult.stderr || changedCommitsResult.stdout || 'no output'}`, { verbose: true });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return true;
|
|
376
|
+
};
|
|
377
|
+
|
|
364
378
|
// Revert the CLAUDE.md or .gitkeep commit to restore original state
|
|
365
379
|
export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash = null) => {
|
|
366
380
|
try {
|
|
@@ -406,6 +420,16 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
|
|
|
406
420
|
|
|
407
421
|
const commitToRevert = claudeCommitHash;
|
|
408
422
|
|
|
423
|
+
// Issue #1791: .gitkeep is a normal repository file in some projects, and
|
|
424
|
+
// user work may intentionally edit or delete it. Once later PR commits touch
|
|
425
|
+
// .gitkeep, final cleanup must not restore the pre-session version.
|
|
426
|
+
if (fileName === '.gitkeep' && (await wasFileTouchedAfterCommit(tempDir, commitToRevert, fileName))) {
|
|
427
|
+
await log(` ${fileName} changed after the initial auto-commit; leaving PR changes untouched`, {
|
|
428
|
+
verbose: true,
|
|
429
|
+
});
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
409
433
|
// APPROACH 3: Check for modifications before reverting (proactive detection)
|
|
410
434
|
// This is the main strategy - detect if the file was modified after initial commit
|
|
411
435
|
await log(` Checking if ${fileName} was modified since initial commit...`, { verbose: true });
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -40,7 +40,7 @@ const { applySolveToolAlias, getFirstParsedPositionalArg, getSolveCommandNameFro
|
|
|
40
40
|
const { executeStartScreen: executeStartScreenCommand, buildExecuteAndUpdateMessage } = await import('./telegram-command-execution.lib.mjs');
|
|
41
41
|
const { isChatStopped, getChatStopInfo, getStoppedChatRejectMessage, DEFAULT_STOP_REASON } = await import('./telegram-start-stop-command.lib.mjs');
|
|
42
42
|
const { isOldMessage: _isOldMessage, isGroupChat: _isGroupChat, isChatAuthorized: _isChatAuthorized, isForwardedOrReply: _isForwardedOrReply, extractCommandFromText, extractGitHubUrl: _extractGitHubUrl } = await import('./telegram-message-filters.lib.mjs');
|
|
43
|
-
const { safeReply } = await import('./telegram-safe-reply.lib.mjs');
|
|
43
|
+
const { installTelegramFormattingFallback, safeEditMessageText, safeReply } = await import('./telegram-safe-reply.lib.mjs');
|
|
44
44
|
const { registerTerminalWatchCommand, startAutoTerminalWatchForSession } = await import('./telegram-terminal-watch-command.lib.mjs');
|
|
45
45
|
const { launchBotWithRetry } = await import('./telegram-bot-launcher.lib.mjs');
|
|
46
46
|
const { trackSession, startSessionMonitoring, hasActiveSessionForUrlAsync } = await import('./session-monitor.lib.mjs');
|
|
@@ -315,6 +315,7 @@ const { Telegraf } = telegrafModule;
|
|
|
315
315
|
const bot = new Telegraf(BOT_TOKEN, {
|
|
316
316
|
handlerTimeout: Infinity, // Remove default 90s timeout; command handlers like /solve spawn long-running processes
|
|
317
317
|
});
|
|
318
|
+
installTelegramFormattingFallback(bot.telegram, { verbose: VERBOSE });
|
|
318
319
|
|
|
319
320
|
// Track bot startup time (Unix seconds to match Telegram's message.date format)
|
|
320
321
|
const BOT_START_TIME = Math.floor(Date.now() / 1000);
|
|
@@ -514,7 +515,7 @@ bot.command('help', async ctx => {
|
|
|
514
515
|
allowTopicHint: topicId ? `TELEGRAM_ALLOWED_TOPICS="(${chatId} ${topicId})"` : '',
|
|
515
516
|
});
|
|
516
517
|
|
|
517
|
-
await ctx
|
|
518
|
+
await safeReply(ctx, message, { fallbackLocale: helpLocale });
|
|
518
519
|
});
|
|
519
520
|
|
|
520
521
|
bot.command('limits', async ctx => {
|
|
@@ -564,10 +565,10 @@ bot.command('limits', async ctx => {
|
|
|
564
565
|
const claudeError = limits.claude.success ? null : limits.claude.error;
|
|
565
566
|
const codexError = limits.codex.success ? null : limits.codex.error;
|
|
566
567
|
const solveQueue = getSolveQueue({ verbose: VERBOSE });
|
|
567
|
-
const queueStatus = await solveQueue.formatStatus();
|
|
568
|
+
const queueStatus = await solveQueue.formatStatus({ locale: userLocale });
|
|
568
569
|
const codexSection = formatCodexLimitsSection(limits.codex.success ? limits.codex : null, codexError, { locale: userLocale });
|
|
569
570
|
const message = t('telegram.usage_limits_title', {}, { locale: userLocale }) + '\n\n' + formatUsageMessage(limits.claude.success ? limits.claude.usage : null, limits.disk.success ? limits.disk.diskSpace : null, limits.github.success ? limits.github.githubRateLimit : null, limits.cpu.success ? limits.cpu.cpuLoad : null, limits.memory.success ? limits.memory.memory : null, claudeError, [codexSection, queueStatus], { locale: userLocale });
|
|
570
|
-
await ctx.telegram
|
|
571
|
+
await safeEditMessageText(ctx.telegram, fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'Markdown', fallbackLocale: userLocale, verbose: VERBOSE });
|
|
571
572
|
});
|
|
572
573
|
bot.command('version', async ctx => {
|
|
573
574
|
VERBOSE && console.log('[VERBOSE] /version command received');
|
|
@@ -585,8 +586,8 @@ bot.command('version', async ctx => {
|
|
|
585
586
|
reply_to_message_id: ctx.message.message_id,
|
|
586
587
|
});
|
|
587
588
|
const result = await getVersionInfo(VERBOSE);
|
|
588
|
-
if (!result.success) return await ctx.telegram
|
|
589
|
-
await ctx.telegram
|
|
589
|
+
if (!result.success) return await safeEditMessageText(ctx.telegram, fetchingMessage.chat.id, fetchingMessage.message_id, undefined, `❌ ${escapeMarkdownV2(result.error, { preserveCodeBlocks: true })}`, { parse_mode: 'MarkdownV2', fallbackLocale: versionLocale, verbose: VERBOSE });
|
|
590
|
+
await safeEditMessageText(ctx.telegram, fetchingMessage.chat.id, fetchingMessage.message_id, undefined, t('telegram.version_information_title', {}, { locale: versionLocale }) + '\n\n' + formatVersionMessage(result.versions, { locale: versionLocale }), { parse_mode: 'Markdown', fallbackLocale: versionLocale, verbose: VERBOSE });
|
|
590
591
|
});
|
|
591
592
|
|
|
592
593
|
const { registerLanguageCommand } = await import('./telegram-language-command.lib.mjs');
|
|
@@ -598,7 +599,7 @@ registerAcceptInvitesCommand(bot, sharedCommandOpts);
|
|
|
598
599
|
const { registerMergeCommand } = await import('./telegram-merge-command.lib.mjs');
|
|
599
600
|
registerMergeCommand(bot, sharedCommandOpts);
|
|
600
601
|
const { registerSolveQueueCommand } = await import('./telegram-solve-queue-command.lib.mjs');
|
|
601
|
-
const { handleSolveQueueCommand } = registerSolveQueueCommand(bot, { ...sharedCommandOpts, getSolveQueue });
|
|
602
|
+
const { handleSolveQueueCommand } = registerSolveQueueCommand(bot, { ...sharedCommandOpts, getSolveQueue, safeReply, resolveLocale: resolveLocaleFromTelegramCtx });
|
|
602
603
|
const { registerSubscribeCommands } = await import('./telegram-subscribers.lib.mjs'); // #1688
|
|
603
604
|
registerSubscribeCommands(bot, sharedCommandOpts);
|
|
604
605
|
const { registerTaskCommands } = await import('./telegram-task-command.lib.mjs');
|
|
@@ -848,7 +849,7 @@ async function handleSolveCommand(ctx) {
|
|
|
848
849
|
await safeReply(ctx, t('telegram.url_session_running', { url: escapeMarkdown(normalizedUrl), session: activeSession.sessionName }, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
|
|
849
850
|
return;
|
|
850
851
|
}
|
|
851
|
-
const check = await solveQueue.canStartCommand({ tool: solveTool }); // Skip Claude limits for agent (#1159)
|
|
852
|
+
const check = await solveQueue.canStartCommand({ tool: solveTool, locale: solveLocale }); // Skip Claude limits for agent (#1159)
|
|
852
853
|
const queueStats = solveQueue.getStats();
|
|
853
854
|
// Handle rejection: threshold strategy is 'reject' — fail immediately (issue #1267)
|
|
854
855
|
if (check.rejected) {
|
|
@@ -1,19 +1,122 @@
|
|
|
1
|
-
|
|
1
|
+
import { normalizeLocale, t } from './i18n.lib.mjs';
|
|
2
|
+
|
|
3
|
+
const FORMATTING_FALLBACK_INSTALLED = Symbol.for('hiveMind.telegramFormattingFallbackInstalled');
|
|
4
|
+
const DEFAULT_FORMATTING_FALLBACK_WARNING = '⚠️ Formatting error detected. Showing plain text fallback.';
|
|
5
|
+
const FORMATTING_FALLBACK_WARNINGS = {
|
|
6
|
+
en: DEFAULT_FORMATTING_FALLBACK_WARNING,
|
|
7
|
+
ru: '⚠️ Обнаружена ошибка форматирования. Показываю обычный текст.',
|
|
8
|
+
zh: '⚠️ 检测到格式错误。正在显示纯文本备用内容。',
|
|
9
|
+
hi: '⚠️ फ़ॉर्मैटिंग त्रुटि मिली। सादा पाठ fallback दिखाया जा रहा है।',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function splitOptions(options = {}) {
|
|
13
|
+
const { fallbackLocale, locale, verbose, ...telegramOptions } = options || {};
|
|
14
|
+
return {
|
|
15
|
+
telegramOptions,
|
|
16
|
+
fallbackLocale: fallbackLocale || locale || null,
|
|
17
|
+
verbose: Boolean(verbose),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isTelegramFormattingError(error) {
|
|
22
|
+
const message = error?.description || error?.message || String(error || '');
|
|
23
|
+
return /can't parse entities/i.test(message) || /can't find end of/i.test(message) || /entity.*parse/i.test(message) || (/bad request/i.test(message) && /400|parse|entity/i.test(message));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function stripTelegramMarkdown(text) {
|
|
27
|
+
return String(text ?? '')
|
|
28
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)')
|
|
29
|
+
.replace(/```([\s\S]*?)```/g, '$1')
|
|
30
|
+
.replace(/\\_/g, '_')
|
|
31
|
+
.replace(/\\\*/g, '*')
|
|
32
|
+
.replace(/\\`/g, '`')
|
|
33
|
+
.replace(/\\\[/g, '[')
|
|
34
|
+
.replace(/\*([^*\n]+)\*/g, '$1')
|
|
35
|
+
.replace(/_([^_\n]+)_/g, '$1')
|
|
36
|
+
.replace(/`([^`]+)`/g, '$1');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getFormattingFallbackWarning(locale) {
|
|
40
|
+
const key = 'telegram.formatting_fallback_warning';
|
|
41
|
+
const normalizedLocale = normalizeLocale(locale);
|
|
42
|
+
const warning = t(key, {}, locale ? { locale } : {});
|
|
43
|
+
if (warning !== key && (normalizedLocale === 'en' || warning !== DEFAULT_FORMATTING_FALLBACK_WARNING)) return warning;
|
|
44
|
+
return FORMATTING_FALLBACK_WARNINGS[normalizedLocale] || DEFAULT_FORMATTING_FALLBACK_WARNING;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function buildTelegramFormattingFallbackText(text, options = {}) {
|
|
48
|
+
const locale = options?.fallbackLocale || options?.locale || null;
|
|
49
|
+
return `${getFormattingFallbackWarning(locale)}\n\n${stripTelegramMarkdown(text)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function logFormattingFailure(scope, error, text, verbose = false) {
|
|
53
|
+
const message = error?.description || error?.message || String(error || '');
|
|
54
|
+
console.error(`[telegram-bot] ${scope}: formatted Telegram message failed: ${message}`);
|
|
55
|
+
if (verbose) {
|
|
56
|
+
console.error(`[telegram-bot] ${scope}: Failing message (${Buffer.byteLength(String(text ?? ''), 'utf-8')} bytes): ${text}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Issue #1460/#1497/#1788: try Markdown first, fall back to localized plain text on parsing errors.
|
|
2
61
|
export async function safeReply(ctx, text, options = {}) {
|
|
62
|
+
const { telegramOptions, fallbackLocale, verbose } = splitOptions(options);
|
|
63
|
+
const firstOptions = { parse_mode: 'Markdown', ...telegramOptions };
|
|
3
64
|
try {
|
|
4
|
-
return await ctx.reply(text,
|
|
65
|
+
return await ctx.reply(text, firstOptions);
|
|
5
66
|
} catch (error) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
console.error(`[telegram-bot] safeReply: Failing message (${Buffer.byteLength(text, 'utf-8')} bytes): ${text}`);
|
|
11
|
-
const plainText = text
|
|
12
|
-
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)')
|
|
13
|
-
.replace(/\\_/g, '_')
|
|
14
|
-
.replace(/\\\*/g, '*')
|
|
15
|
-
.replace(/\*([^*]+)\*/g, '$1')
|
|
16
|
-
.replace(/`([^`]+)`/g, '$1');
|
|
17
|
-
return await ctx.reply(plainText, { ...options, parse_mode: undefined });
|
|
67
|
+
if (!isTelegramFormattingError(error)) throw error;
|
|
68
|
+
logFormattingFailure('safeReply', error, text, verbose);
|
|
69
|
+
const fallbackText = buildTelegramFormattingFallbackText(text, { fallbackLocale });
|
|
70
|
+
return await ctx.reply(fallbackText, { ...telegramOptions, parse_mode: undefined });
|
|
18
71
|
}
|
|
19
72
|
}
|
|
73
|
+
|
|
74
|
+
export async function safeEditMessageText(telegram, chatId, messageId, inlineMessageId, text, options = {}) {
|
|
75
|
+
const { telegramOptions, fallbackLocale, verbose } = splitOptions(options);
|
|
76
|
+
const firstOptions = { parse_mode: 'Markdown', ...telegramOptions };
|
|
77
|
+
try {
|
|
78
|
+
return await telegram.editMessageText(chatId, messageId, inlineMessageId, text, firstOptions);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (!isTelegramFormattingError(error)) throw error;
|
|
81
|
+
logFormattingFailure('safeEditMessageText', error, text, verbose);
|
|
82
|
+
const fallbackText = buildTelegramFormattingFallbackText(text, { fallbackLocale });
|
|
83
|
+
return await telegram.editMessageText(chatId, messageId, inlineMessageId, fallbackText, { ...telegramOptions, parse_mode: undefined });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function wrapTelegramMethod(telegram, methodName, textIndex, optionsIndex, defaults = {}) {
|
|
88
|
+
const original = telegram?.[methodName];
|
|
89
|
+
if (typeof original !== 'function') return;
|
|
90
|
+
|
|
91
|
+
telegram[methodName] = async function wrappedTelegramMessageMethod(...args) {
|
|
92
|
+
const text = args[textIndex];
|
|
93
|
+
const originalOptions = args[optionsIndex] || {};
|
|
94
|
+
const { telegramOptions, fallbackLocale, verbose } = splitOptions(originalOptions);
|
|
95
|
+
args[optionsIndex] = telegramOptions;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
return await original.apply(this, args);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (!isTelegramFormattingError(error) || typeof text !== 'string') throw error;
|
|
101
|
+
logFormattingFailure(methodName, error, text, verbose || defaults.verbose);
|
|
102
|
+
const retryArgs = [...args];
|
|
103
|
+
retryArgs[textIndex] = buildTelegramFormattingFallbackText(text, { fallbackLocale: fallbackLocale || defaults.fallbackLocale });
|
|
104
|
+
retryArgs[optionsIndex] = { ...telegramOptions, parse_mode: undefined };
|
|
105
|
+
return await original.apply(this, retryArgs);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function installTelegramFormattingFallback(telegram, options = {}) {
|
|
111
|
+
if (!telegram || telegram[FORMATTING_FALLBACK_INSTALLED]) return telegram;
|
|
112
|
+
|
|
113
|
+
const defaults = {
|
|
114
|
+
fallbackLocale: options.fallbackLocale || options.locale || null,
|
|
115
|
+
verbose: Boolean(options.verbose),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
wrapTelegramMethod(telegram, 'sendMessage', 1, 2, defaults);
|
|
119
|
+
wrapTelegramMethod(telegram, 'editMessageText', 3, 4, defaults);
|
|
120
|
+
telegram[FORMATTING_FALLBACK_INSTALLED] = true;
|
|
121
|
+
return telegram;
|
|
122
|
+
}
|
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
* @see https://github.com/link-assistant/hive-mind/issues/1232
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { t } from './i18n.lib.mjs';
|
|
17
|
+
|
|
18
|
+
const GROUP_ONLY_MESSAGE = '❌ The /solve_queue command only works in group chats. Please add this bot to a group and make it an admin.';
|
|
19
|
+
|
|
20
|
+
function commandText(key, params = {}, locale = null, fallback = key) {
|
|
21
|
+
const translated = t(key, params, locale ? { locale } : {});
|
|
22
|
+
return translated === key ? fallback : translated;
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
/**
|
|
17
26
|
* Registers the /solve_queue command handler with the bot
|
|
18
27
|
* @param {Object} bot - The Telegraf bot instance
|
|
@@ -29,7 +38,7 @@
|
|
|
29
38
|
* @returns {{ handleSolveQueueCommand: Function }} The command handler for use in text fallback
|
|
30
39
|
*/
|
|
31
40
|
export function registerSolveQueueCommand(bot, options) {
|
|
32
|
-
const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized, isTopicAuthorized, buildAuthErrorMessage, addBreadcrumb, getSolveQueue } = options;
|
|
41
|
+
const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized, isTopicAuthorized, buildAuthErrorMessage, addBreadcrumb, getSolveQueue, safeReply, resolveLocale } = options;
|
|
33
42
|
|
|
34
43
|
async function handleSolveQueueCommand(ctx) {
|
|
35
44
|
VERBOSE && console.log('[VERBOSE] /solve_queue command received');
|
|
@@ -40,6 +49,8 @@ export function registerSolveQueueCommand(bot, options) {
|
|
|
40
49
|
level: 'info',
|
|
41
50
|
data: { chatId: ctx.chat?.id, chatType: ctx.chat?.type, userId: ctx.from?.id, username: ctx.from?.username },
|
|
42
51
|
});
|
|
52
|
+
const locale = resolveLocale ? resolveLocale(ctx) : null;
|
|
53
|
+
const replyWithFallback = (text, replyOptions = {}) => (safeReply ? safeReply(ctx, text, replyOptions) : ctx.reply(text, { parse_mode: 'Markdown', ...replyOptions }));
|
|
43
54
|
|
|
44
55
|
// Ignore messages sent before bot started
|
|
45
56
|
if (isOldMessage(ctx)) {
|
|
@@ -55,8 +66,9 @@ export function registerSolveQueueCommand(bot, options) {
|
|
|
55
66
|
|
|
56
67
|
if (!isGroupChat(ctx)) {
|
|
57
68
|
VERBOSE && console.log('[VERBOSE] /solve_queue ignored: not a group chat');
|
|
58
|
-
await
|
|
69
|
+
await replyWithFallback(commandText('telegram.solve_queue_only_in_groups', {}, locale, GROUP_ONLY_MESSAGE), {
|
|
59
70
|
reply_to_message_id: ctx.message.message_id,
|
|
71
|
+
fallbackLocale: locale,
|
|
60
72
|
});
|
|
61
73
|
return;
|
|
62
74
|
}
|
|
@@ -65,7 +77,7 @@ export function registerSolveQueueCommand(bot, options) {
|
|
|
65
77
|
if (!authorize(ctx)) {
|
|
66
78
|
VERBOSE && console.log('[VERBOSE] /solve_queue ignored: not authorized');
|
|
67
79
|
const errMsg = buildAuthErrorMessage ? buildAuthErrorMessage(ctx) : `❌ This chat (ID: ${ctx.chat.id}) is not authorized.`;
|
|
68
|
-
await
|
|
80
|
+
await replyWithFallback(errMsg, { reply_to_message_id: ctx.message.message_id, fallbackLocale: locale });
|
|
69
81
|
return;
|
|
70
82
|
}
|
|
71
83
|
|
|
@@ -77,11 +89,11 @@ export function registerSolveQueueCommand(bot, options) {
|
|
|
77
89
|
// Shows per-queue breakdown with first 5 items per queue and human-readable times
|
|
78
90
|
// Processing counts are actual running system processes (via pgrep)
|
|
79
91
|
// See: https://github.com/link-assistant/hive-mind/issues/1267
|
|
80
|
-
const message = await solveQueue.formatDetailedStatus();
|
|
92
|
+
const message = await solveQueue.formatDetailedStatus({ locale });
|
|
81
93
|
|
|
82
|
-
await
|
|
83
|
-
parse_mode: 'Markdown',
|
|
94
|
+
await replyWithFallback(message, {
|
|
84
95
|
reply_to_message_id: ctx.message.message_id,
|
|
96
|
+
fallbackLocale: locale,
|
|
85
97
|
});
|
|
86
98
|
}
|
|
87
99
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { exec } from 'node:child_process';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
|
+
import { lt } from './limits-i18n.lib.mjs';
|
|
5
6
|
|
|
6
7
|
const execAsync = promisify(exec);
|
|
7
8
|
|
|
@@ -111,8 +112,9 @@ export function formatThresholdPercent(ratio) {
|
|
|
111
112
|
* @returns {string} Human-readable duration
|
|
112
113
|
* @see https://github.com/link-assistant/hive-mind/issues/1267
|
|
113
114
|
*/
|
|
114
|
-
export function formatDuration(ms) {
|
|
115
|
+
export function formatDuration(ms, options = {}) {
|
|
115
116
|
if (ms < 0) ms = 0;
|
|
117
|
+
const locale = typeof options === 'string' ? options : options?.locale || null;
|
|
116
118
|
|
|
117
119
|
const totalSeconds = Math.floor(ms / 1000);
|
|
118
120
|
const days = Math.floor(totalSeconds / 86400);
|
|
@@ -120,11 +122,26 @@ export function formatDuration(ms) {
|
|
|
120
122
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
121
123
|
const seconds = totalSeconds % 60;
|
|
122
124
|
|
|
125
|
+
const labels =
|
|
126
|
+
locale && locale !== 'en'
|
|
127
|
+
? {
|
|
128
|
+
day: lt('duration_day_short', {}, { locale }),
|
|
129
|
+
hour: lt('duration_hour_short', {}, { locale }),
|
|
130
|
+
minute: lt('duration_minute_short', {}, { locale }),
|
|
131
|
+
second: lt('duration_second_short', {}, { locale }),
|
|
132
|
+
}
|
|
133
|
+
: {
|
|
134
|
+
day: 'd',
|
|
135
|
+
hour: 'h',
|
|
136
|
+
minute: 'm',
|
|
137
|
+
second: 's',
|
|
138
|
+
};
|
|
139
|
+
|
|
123
140
|
const parts = [];
|
|
124
|
-
if (days > 0) parts.push(`${days}
|
|
125
|
-
if (hours > 0) parts.push(`${hours}
|
|
126
|
-
if (minutes > 0) parts.push(`${minutes}
|
|
127
|
-
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}
|
|
141
|
+
if (days > 0) parts.push(`${days}${locale && locale !== 'en' ? ' ' : ''}${labels.day}`);
|
|
142
|
+
if (hours > 0) parts.push(`${hours}${locale && locale !== 'en' ? ' ' : ''}${labels.hour}`);
|
|
143
|
+
if (minutes > 0) parts.push(`${minutes}${locale && locale !== 'en' ? ' ' : ''}${labels.minute}`);
|
|
144
|
+
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}${locale && locale !== 'en' ? ' ' : ''}${labels.second}`);
|
|
128
145
|
|
|
129
146
|
return parts.join(' ');
|
|
130
147
|
}
|
|
@@ -136,9 +153,44 @@ export function formatDuration(ms) {
|
|
|
136
153
|
* @param {number} threshold - Threshold ratio (0.0 - 1.0)
|
|
137
154
|
* @returns {string} Human-readable reason
|
|
138
155
|
*/
|
|
139
|
-
export function formatWaitingReason(metric, currentValue, threshold) {
|
|
156
|
+
export function formatWaitingReason(metric, currentValue, threshold, options = {}) {
|
|
157
|
+
const locale = typeof options === 'string' ? options : options?.locale || null;
|
|
140
158
|
const thresholdPercent = formatThresholdPercent(threshold);
|
|
141
159
|
const currentPercent = Math.round(currentValue);
|
|
160
|
+
const params = { currentPercent, thresholdPercent, metric };
|
|
161
|
+
|
|
162
|
+
if (locale && locale !== 'en') {
|
|
163
|
+
switch (metric) {
|
|
164
|
+
case 'ram':
|
|
165
|
+
return lt('reason_ram_usage', params, { locale });
|
|
166
|
+
case 'cpu':
|
|
167
|
+
return lt('reason_cpu_usage', params, { locale });
|
|
168
|
+
case 'disk':
|
|
169
|
+
return lt('reason_disk_usage', params, { locale });
|
|
170
|
+
case 'claude_5_hour_session':
|
|
171
|
+
return lt('reason_claude_5_hour_session', params, { locale });
|
|
172
|
+
case 'claude_weekly':
|
|
173
|
+
return lt('reason_claude_weekly', params, { locale });
|
|
174
|
+
case 'codex_5_hour_session':
|
|
175
|
+
return lt('reason_codex_5_hour_session', params, { locale });
|
|
176
|
+
case 'codex_weekly':
|
|
177
|
+
return lt('reason_codex_weekly', params, { locale });
|
|
178
|
+
case 'github':
|
|
179
|
+
return lt('reason_github_api', params, { locale });
|
|
180
|
+
case 'min_interval':
|
|
181
|
+
return lt('reason_min_interval', params, { locale });
|
|
182
|
+
case 'claude_running':
|
|
183
|
+
return lt('reason_claude_running', params, { locale });
|
|
184
|
+
case 'codex_running':
|
|
185
|
+
return lt('reason_codex_running', params, { locale });
|
|
186
|
+
case 'qwen_running':
|
|
187
|
+
return lt('reason_qwen_running', params, { locale });
|
|
188
|
+
case 'gemini_running':
|
|
189
|
+
return lt('reason_gemini_running', params, { locale });
|
|
190
|
+
default:
|
|
191
|
+
return lt('reason_threshold_exceeded', params, { locale });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
142
194
|
|
|
143
195
|
switch (metric) {
|
|
144
196
|
case 'ram':
|