From abdce0513601cb971244a746492cf6a982a761202be3f81159f940661b9836a2 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Sun, 24 May 2026 09:30:25 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D1=82=D1=8C=20Ja?= =?UTF-8?q?va-=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8E?= =?UTF-8?q?=20=D0=B0=D0=B3=D0=B5=D0=BD=D1=82=D0=B0-=D0=BA=D0=BE=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...24_0040_агент_бот_coder_очередь_systemd.md | 18 - ...агент_бот_устранение_дублей_и_зависаний.md | 22 - ...-05-24_0928_удаление_java_агента_кодера.md | 16 + SHiNE-agent-bot-coder/AGENT.md | 2 +- SHiNE-agent-bot-coder/README.md | 4 +- SHiNE-agent-bot-coder/build.gradle | 50 -- .../agent/botcoder/BotCoderApplication.java | 76 --- .../agent/botcoder/codex/CodexClient.java | 225 ------ .../botcoder/codex/CodexStatusListener.java | 5 - .../agent/botcoder/config/AppConfig.java | 88 --- .../agent/botcoder/config/EnvLoader.java | 45 -- .../botcoder/history/HistoryManager.java | 162 ----- .../botcoder/openai/OpenAiTranscriber.java | 81 --- .../agent/botcoder/queue/FailureResult.java | 4 - .../shine/agent/botcoder/queue/QueueJob.java | 54 -- .../agent/botcoder/queue/QueueStatus.java | 6 - .../agent/botcoder/queue/QueueStore.java | 203 ------ .../agent/botcoder/state/RuntimeState.java | 10 - .../botcoder/state/RuntimeStateStore.java | 75 -- .../botcoder/state/SingleInstanceLock.java | 50 -- .../telegram/ProcessedUpdatesStore.java | 75 -- .../botcoder/telegram/ShineAgentBot.java | 645 ------------------ VERSION.properties | 4 +- settings.gradle | 1 - 24 files changed, 21 insertions(+), 1900 deletions(-) delete mode 100644 Dev_Docs/Pending_Features/2026-05-24_0040_агент_бот_coder_очередь_systemd.md delete mode 100644 Dev_Docs/Pending_Features/2026-05-24_0247_агент_бот_устранение_дублей_и_зависаний.md create mode 100644 Dev_Docs/Pending_Features/2026-05-24_0928_удаление_java_агента_кодера.md delete mode 100644 SHiNE-agent-bot-coder/build.gradle delete mode 100644 SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/BotCoderApplication.java delete mode 100644 SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/codex/CodexClient.java delete mode 100644 SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/codex/CodexStatusListener.java delete mode 100644 SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/config/AppConfig.java delete mode 100644 SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/config/EnvLoader.java delete mode 100644 SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/history/HistoryManager.java delete mode 100644 SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/openai/OpenAiTranscriber.java delete mode 100644 SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/queue/FailureResult.java delete mode 100644 SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/queue/QueueJob.java delete mode 100644 SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/queue/QueueStatus.java delete mode 100644 SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/queue/QueueStore.java delete mode 100644 SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/state/RuntimeState.java delete mode 100644 SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/state/RuntimeStateStore.java delete mode 100644 SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/state/SingleInstanceLock.java delete mode 100644 SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/telegram/ProcessedUpdatesStore.java delete mode 100644 SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/telegram/ShineAgentBot.java diff --git a/Dev_Docs/Pending_Features/2026-05-24_0040_агент_бот_coder_очередь_systemd.md b/Dev_Docs/Pending_Features/2026-05-24_0040_агент_бот_coder_очередь_systemd.md deleted file mode 100644 index 41efcc7..0000000 --- a/Dev_Docs/Pending_Features/2026-05-24_0040_агент_бот_coder_очередь_systemd.md +++ /dev/null @@ -1,18 +0,0 @@ -# SHiNE-agent-bot-coder: очередь, voice, codex, systemd - -- краткое описание фичи: - Добавлен новый сервис `SHiNE-agent-bot-coder` (Java), который обрабатывает сообщения от `@AidarKC`, ведёт JSONL-историю, использует файловую очередь, распознаёт voice через OpenAI, вызывает Codex CLI и поддерживает запуск как `systemd`-сервис. - -- что именно проверять: - 1. Бот принимает текст от `@AidarKC`, ставит задачу в очередь и отправляет ответ от Codex. - 2. Бот принимает voice, отправляет текст распознавания и затем ответ от Codex. - 3. Одновременные сообщения обрабатываются строго по одному (без параллельных запусков Codex). - 4. После рестарта сервиса незавершённая активная задача повторно уходит в обработку. - 5. Команда `/new` архивирует текущую историю и создаёт новую. - 6. `systemd`-сервис стартует и автоматически перезапускается. - -- ожидаемый результат: - Все пункты выше отрабатывают без потери сообщений, с корректным обновлением `data/queue.jsonl`, `data/state.json` и `data/history/*.jsonl`. - -- статус: - `pending` diff --git a/Dev_Docs/Pending_Features/2026-05-24_0247_агент_бот_устранение_дублей_и_зависаний.md b/Dev_Docs/Pending_Features/2026-05-24_0247_агент_бот_устранение_дублей_и_зависаний.md deleted file mode 100644 index 1c9d9c9..0000000 --- a/Dev_Docs/Pending_Features/2026-05-24_0247_агент_бот_устранение_дублей_и_зависаний.md +++ /dev/null @@ -1,22 +0,0 @@ -# Агент-бот coder: устранение дублей и зависаний - -- краткое описание фичи: - - добавлен lock-файл `data/app.lock`, чтобы гарантировать единственный инстанс бота; - - доработано завершение worker/codex при stop/restart, чтобы не было ложных retry после штатной остановки; - - синхронизирован systemd-профиль под `--user` для `ai`; - - улучшена отправка промежуточных статусов по событиям `codex --json`. - -- что проверять: - - при запущенном сервисе повторный запуск jar вручную завершается сразу с сообщением про занятый lock; - - команда `/status` отвечает один раз (без дублей); - - тестовая текстовая задача от `@AidarKC` обрабатывается и возвращает ответ; - - при `/stop` активная задача завершается без последующего спама retry-ошибками; - - в логах нет `409 Conflict` из-за второго poller после чистого перезапуска. - -- ожидаемый результат: - - одновременно работает только один процесс бота; - - бот не дублирует ответы; - - очередь не застревает на interrupted после штатного stop/restart. - -- статус: - - pending diff --git a/Dev_Docs/Pending_Features/2026-05-24_0928_удаление_java_агента_кодера.md b/Dev_Docs/Pending_Features/2026-05-24_0928_удаление_java_агента_кодера.md new file mode 100644 index 0000000..d95e40a --- /dev/null +++ b/Dev_Docs/Pending_Features/2026-05-24_0928_удаление_java_агента_кодера.md @@ -0,0 +1,16 @@ +# Удаление старой Java-реализации агента-кодера + +- краткое описание фичи: + Старая Java-реализация `SHiNE-agent-bot-coder` удалена, потому что рабочим вариантом сервиса является Python-скрипт `py_bot_service.py`. + +- что именно проверять: + 1. Gradle-проект больше не содержит подпроект `SHiNE-agent-bot-coder`. + 2. Локальный systemd-сервис `shine-agent-bot-coder` запускает `py_bot_service.py`. + 3. Telegram-бот принимает сообщение от Айдара и отвечает через Python-сервис. + 4. Команды `/status`, `/queue`, `/new` и `/restart_service` работают как раньше. + +- ожидаемый результат: + Удаление Java-кода не влияет на текущую работу Python-сервиса агента-кодера. + +- статус: + `pending` diff --git a/SHiNE-agent-bot-coder/AGENT.md b/SHiNE-agent-bot-coder/AGENT.md index 7fec48d..20b4097 100644 --- a/SHiNE-agent-bot-coder/AGENT.md +++ b/SHiNE-agent-bot-coder/AGENT.md @@ -9,7 +9,7 @@ - История диалога хранится в JSONL-файле, путь передаётся в промпте. - Сообщение может быть текстом или результатом распознавания голосового. - Ответ пойдёт пользователю в Telegram как обычное текстовое сообщение. -- Рабочая реализация сервиса — Python-скрипт `py_bot_service.py`; Java-реализацию не считать основной и не использовать как точку запуска без отдельного указания. +- Единственная рабочая реализация сервиса — Python-скрипт `py_bot_service.py`; старая Java-реализация удалена как нерабочая и не должна восстанавливаться без отдельного решения Айдара. ## Авторитет команд и история - Основной пользователь и источник команд — Айдар: `@AidarKC` / `@aidarkc`. diff --git a/SHiNE-agent-bot-coder/README.md b/SHiNE-agent-bot-coder/README.md index 924af9a..d3a3f09 100644 --- a/SHiNE-agent-bot-coder/README.md +++ b/SHiNE-agent-bot-coder/README.md @@ -11,10 +11,10 @@ - отправляет аварийный статус только если Codex молчит 2 минуты подряд во время активной задачи; - принимает сообщения из канала `@shine_writing`, выполняет команды только от `@AidarKC`, а сообщения других авторов сохраняет как контекст. +Рабочая реализация сервиса — только `py_bot_service.py`. Старая Java-реализация удалена, потому что не заработала и больше не используется. + ## Структура - `.env` — локальные секреты и параметры запуска (не коммитится); -- `data/queue.jsonl` — очередь задач; -- `data/state.json` — текущее состояние (active job + текущий history-файл); - `data/py_queue.jsonl` — очередь Python-сервиса; - `data/py_state.json` — текущее состояние Python-сервиса; - `data/py_processed_updates.log` — дедуп входящих update; diff --git a/SHiNE-agent-bot-coder/build.gradle b/SHiNE-agent-bot-coder/build.gradle deleted file mode 100644 index b7b5b0e..0000000 --- a/SHiNE-agent-bot-coder/build.gradle +++ /dev/null @@ -1,50 +0,0 @@ -plugins { - id 'java' - id 'application' - id 'com.github.johnrengelman.shadow' version '8.1.1' -} - -group = 'shine.agent' -version = '1.0.0' - -repositories { - mavenCentral() -} - -dependencies { - implementation 'org.telegram:telegrambots:6.9.7.1' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' - implementation 'org.slf4j:slf4j-api:2.0.16' - runtimeOnly 'org.slf4j:slf4j-simple:2.0.16' - implementation 'org.apache.httpcomponents:httpclient:4.5.14' - implementation 'org.apache.httpcomponents:httpcore:4.4.16' - implementation 'commons-codec:commons-codec:1.17.0' - - testImplementation platform('org.junit:junit-bom:5.10.2') - testImplementation 'org.junit.jupiter:junit-jupiter' -} - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } -} - -application { - mainClass = 'shine.agent.botcoder.BotCoderApplication' -} - -tasks.named('jar') { - enabled = false -} - -shadowJar { - archiveBaseName.set('shine-agent-bot-coder') - archiveClassifier.set('') - archiveVersion.set('') - mergeServiceFiles() -} - -tasks.named('test') { - useJUnitPlatform() -} diff --git a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/BotCoderApplication.java b/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/BotCoderApplication.java deleted file mode 100644 index 896a28b..0000000 --- a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/BotCoderApplication.java +++ /dev/null @@ -1,76 +0,0 @@ -package shine.agent.botcoder; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.telegram.telegrambots.meta.TelegramBotsApi; -import org.telegram.telegrambots.updatesreceivers.DefaultBotSession; -import shine.agent.botcoder.codex.CodexClient; -import shine.agent.botcoder.config.AppConfig; -import shine.agent.botcoder.history.HistoryManager; -import shine.agent.botcoder.openai.OpenAiTranscriber; -import shine.agent.botcoder.queue.QueueStore; -import shine.agent.botcoder.state.RuntimeStateStore; -import shine.agent.botcoder.state.SingleInstanceLock; -import shine.agent.botcoder.telegram.ProcessedUpdatesStore; -import shine.agent.botcoder.telegram.ShineAgentBot; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.concurrent.CountDownLatch; - -public class BotCoderApplication { - - private static final Logger log = LoggerFactory.getLogger(BotCoderApplication.class); - - public static void main(String[] args) throws Exception { - Path serviceRoot = Path.of("").toAbsolutePath().normalize(); - AppConfig config = AppConfig.load(serviceRoot); - Files.createDirectories(config.dataDir()); - SingleInstanceLock appLock = SingleInstanceLock.tryAcquire(config.dataDir().resolve("app.lock")); - if (appLock == null) { - log.error("SHiNE-agent-bot-coder уже запущен: lock занят {}", config.dataDir().resolve("app.lock")); - return; - } - - RuntimeStateStore stateStore = new RuntimeStateStore(config.dataDir().resolve("state.json")); - QueueStore queueStore = new QueueStore(config.dataDir().resolve("queue.jsonl"), stateStore); - HistoryManager historyManager = new HistoryManager( - config.dataDir().resolve("history"), - config.dataDir().resolve("history").resolve("archive"), - stateStore - ); - - List recovered = queueStore.recoverActiveJobs(); - if (!recovered.isEmpty()) { - historyManager.appendSystemEvent("active_jobs_recovered", java.util.Map.of("jobIds", recovered)); - } - - OpenAiTranscriber transcriber = new OpenAiTranscriber(config.openAiApiKey(), config.openAiTranscribeModel()); - CodexClient codexClient = new CodexClient(config.codexBin(), config.codexWorkDir(), config.codexTimeoutSeconds()); - ProcessedUpdatesStore processedUpdatesStore = new ProcessedUpdatesStore( - config.dataDir().resolve("processed_updates.log"), - 5000 - ); - - ShineAgentBot bot = new ShineAgentBot(config, queueStore, historyManager, transcriber, codexClient, processedUpdatesStore); - bot.startWorkers(); - - TelegramBotsApi botsApi = new TelegramBotsApi(DefaultBotSession.class); - botsApi.registerBot(bot); - - log.info("SHiNE-agent-bot-coder запущен. allowed user: @{}", config.allowedTelegramUsername()); - - CountDownLatch latch = new CountDownLatch(1); - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - bot.shutdown(); - try { - appLock.close(); - } catch (Exception e) { - log.warn("Не удалось закрыть lock-файл", e); - } - latch.countDown(); - }, "shine-agent-bot-shutdown")); - latch.await(); - } -} diff --git a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/codex/CodexClient.java b/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/codex/CodexClient.java deleted file mode 100644 index f712c5e..0000000 --- a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/codex/CodexClient.java +++ /dev/null @@ -1,225 +0,0 @@ -package shine.agent.botcoder.codex; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.TimeUnit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CodexClient { - - private static final Logger log = LoggerFactory.getLogger(CodexClient.class); - - private final Path codexBin; - private final Path codexWorkDir; - private final int timeoutSeconds; - private final AtomicReference activeProcess = new AtomicReference<>(); - - public CodexClient(Path codexBin, Path codexWorkDir, int timeoutSeconds) { - this.codexBin = codexBin; - this.codexWorkDir = codexWorkDir; - this.timeoutSeconds = timeoutSeconds; - } - - public String executePrompt(String prompt, CodexStatusListener statusListener) throws IOException, InterruptedException { - Path lastMessageFile = Files.createTempFile("shine-codex-last-message-", ".txt"); - List command = new ArrayList<>(); - command.add(codexBin.toString()); - command.add("exec"); - command.add("--dangerously-bypass-approvals-and-sandbox"); - command.add("--json"); - command.add("-C"); - command.add(codexWorkDir.toString()); - command.add("-o"); - command.add(lastMessageFile.toString()); - command.add(prompt); - log.info("Запуск codex exec, bin={}, workdir={}", codexBin, codexWorkDir); - - ProcessBuilder builder = new ProcessBuilder(command); - builder.redirectErrorStream(true); - Process process = builder.start(); - activeProcess.set(process); - if (statusListener != null) { - statusListener.onStatus("Codex запущен"); - } - - StringBuilder output = new StringBuilder(); - Thread outputThread = new Thread(() -> readOutput(process, output, statusListener)); - outputThread.setDaemon(true); - outputThread.start(); - - boolean finished; - try { - finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS); - } catch (InterruptedException interrupted) { - process.destroyForcibly(); - joinOutputThread(outputThread); - activeProcess.compareAndSet(process, null); - Thread.currentThread().interrupt(); - throw interrupted; - } - - try { - if (!finished) { - process.destroyForcibly(); - joinOutputThread(outputThread); - log.error("Codex timeout after {}s", timeoutSeconds); - throw new IOException("Codex timeout after " + timeoutSeconds + "s"); - } - - joinOutputThread(outputThread); - int exitCode = process.exitValue(); - String lastMessage = ""; - if (Files.exists(lastMessageFile)) { - lastMessage = Files.readString(lastMessageFile, StandardCharsets.UTF_8).trim(); - } - - if (exitCode != 0) { - log.error("Codex exit code={}, outputTail={}", exitCode, tail(output.toString(), 500)); - throw new IOException("Codex exited with code " + exitCode + ". Output: " + tail(output.toString(), 1800)); - } - - if (!lastMessage.isBlank()) { - return lastMessage; - } - - String fallback = extractFallbackMessage(output.toString()); - if (fallback.isBlank()) { - throw new IOException("Codex returned empty response"); - } - return fallback; - } finally { - activeProcess.compareAndSet(process, null); - try { - Files.deleteIfExists(lastMessageFile); - } catch (IOException ignored) { - } - } - } - - public boolean stopActiveProcess() { - Process process = activeProcess.getAndSet(null); - if (process == null) { - return false; - } - process.destroy(); - try { - if (!process.waitFor(2, TimeUnit.SECONDS)) { - process.destroyForcibly(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - process.destroyForcibly(); - } - return true; - } - - private void readOutput(Process process, StringBuilder output, CodexStatusListener statusListener) { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - output.append(line).append('\n'); - String status = normalizeStatusLine(line); - if (status != null && statusListener != null) { - statusListener.onStatus(status); - } - } - } catch (Exception ignored) { - } - } - - private String normalizeStatusLine(String line) { - String trimmed = line == null ? "" : line.trim(); - if (trimmed.isEmpty()) { - return null; - } - if (trimmed.contains("\"type\":\"thread.started\"")) { - return "Codex: инициализировал сессию"; - } - if (trimmed.contains("\"type\":\"turn.started\"")) { - return "Codex: начал обработку запроса"; - } - if (trimmed.contains("\"type\":\"item.completed\"") && trimmed.contains("\"type\":\"agent_message\"")) { - return "Codex: формирует финальный ответ"; - } - if (trimmed.contains("\"type\":\"turn.completed\"")) { - return "Codex: завершил шаг"; - } - if (trimmed.contains("\"type\":\"error\"")) { - return "Codex: ошибка выполнения"; - } - if (trimmed.contains("\"type\":\"reasoning\"")) { - return "Codex: анализирует задачу"; - } - if (trimmed.contains("\"type\":\"function_call\"")) { - return "Codex: вызывает инструмент"; - } - if (trimmed.contains("\"type\":\"function_call_output\"")) { - return "Codex: получил результат инструмента"; - } - if (trimmed.contains("\"type\":\"message\"") && trimmed.contains("\"role\":\"assistant\"")) { - return "Codex: формирует ответ"; - } - if (trimmed.startsWith("mcp")) { - return "Codex: инициализирует MCP"; - } - if (trimmed.startsWith("tokens used")) { - return "Codex: завершает обработку"; - } - if (trimmed.startsWith("ERROR:")) { - return "Codex: ошибка выполнения"; - } - return null; - } - - private void joinOutputThread(Thread outputThread) throws InterruptedException { - try { - outputThread.join(Duration.ofSeconds(2).toMillis()); - } catch (InterruptedException interrupted) { - Thread.currentThread().interrupt(); - throw interrupted; - } - } - - private String extractFallbackMessage(String rawOutput) { - String[] lines = rawOutput.split("\\R"); - for (int i = lines.length - 1; i >= 0; i--) { - String line = lines[i].trim(); - if (line.isEmpty()) { - continue; - } - if (line.startsWith("tokens used")) { - continue; - } - if (line.startsWith("OpenAI Codex")) { - continue; - } - if (line.startsWith("workdir:") || line.startsWith("model:") || line.startsWith("provider:")) { - continue; - } - if (line.startsWith("approval:") || line.startsWith("sandbox:") || line.startsWith("reasoning")) { - continue; - } - if (line.equals("user") || line.equals("exec") || line.equals("--------")) { - continue; - } - return line; - } - return ""; - } - - private String tail(String value, int maxLen) { - if (value.length() <= maxLen) { - return value; - } - return value.substring(value.length() - maxLen); - } -} diff --git a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/codex/CodexStatusListener.java b/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/codex/CodexStatusListener.java deleted file mode 100644 index e5b8ba8..0000000 --- a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/codex/CodexStatusListener.java +++ /dev/null @@ -1,5 +0,0 @@ -package shine.agent.botcoder.codex; - -public interface CodexStatusListener { - void onStatus(String message); -} diff --git a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/config/AppConfig.java b/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/config/AppConfig.java deleted file mode 100644 index ae978fd..0000000 --- a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/config/AppConfig.java +++ /dev/null @@ -1,88 +0,0 @@ -package shine.agent.botcoder.config; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Map; - -public record AppConfig( - String telegramBotToken, - String botUsername, - String allowedTelegramUsername, - String openAiApiKey, - String openAiTranscribeModel, - Path codexBin, - Path codexWorkDir, - int codexTimeoutSeconds, - int maxRetries, - Path dataDir, - Path agentInstructionsFile -) { - - public static AppConfig load(Path serviceRoot) throws IOException { - Map env = EnvLoader.load(serviceRoot.resolve(".env")); - String telegramBotToken = required(env, "TELEGRAM_BOT_TOKEN"); - String openAiApiKey = required(env, "OPENAI_API_KEY"); - - String botUsername = env.getOrDefault("BOT_USERNAME", "aidar_su_bot"); - String allowed = normalizeUsername(env.getOrDefault("ALLOWED_TELEGRAM_USERNAME", "AidarKC")); - String transcribeModel = env.getOrDefault("OPENAI_TRANSCRIBE_MODEL", "gpt-4o-mini-transcribe"); - - Path codexBin = Path.of(env.getOrDefault( - "CODEX_BIN", - "/home/ai/.cache/JetBrains/IntelliJIdea2026.1/aia/codex/bin/codex-x86_64-unknown-linux-musl" - )); - Path codexWorkDir = Path.of(env.getOrDefault( - "CODEX_WORKDIR", - "/home/ai/work/SHiNE/SHiNE-server-sha256" - )); - - int timeout = parseInt(env.getOrDefault("CODEX_TIMEOUT_SECONDS", "900"), 900); - int retries = parseInt(env.getOrDefault("MAX_RETRIES", "3"), 3); - if (retries < 1) { - retries = 1; - } - Path dataDir = serviceRoot.resolve(env.getOrDefault("DATA_DIR", "./data")).normalize(); - Path agentInstructions = serviceRoot.resolve("AGENT.md").normalize(); - - return new AppConfig( - telegramBotToken, - botUsername, - allowed, - openAiApiKey, - transcribeModel, - codexBin, - codexWorkDir, - timeout, - retries, - dataDir, - agentInstructions - ); - } - - public static String normalizeUsername(String value) { - if (value == null) { - return ""; - } - String normalized = value.trim(); - if (normalized.startsWith("@")) { - normalized = normalized.substring(1); - } - return normalized.toLowerCase(); - } - - private static String required(Map env, String key) { - String value = env.get(key); - if (value == null || value.isBlank()) { - throw new IllegalArgumentException("Не задан обязательный параметр: " + key); - } - return value.trim(); - } - - private static int parseInt(String value, int fallback) { - try { - return Integer.parseInt(value.trim()); - } catch (Exception ignored) { - return fallback; - } - } -} diff --git a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/config/EnvLoader.java b/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/config/EnvLoader.java deleted file mode 100644 index f770bf8..0000000 --- a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/config/EnvLoader.java +++ /dev/null @@ -1,45 +0,0 @@ -package shine.agent.botcoder.config; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.Map; - -public final class EnvLoader { - - private EnvLoader() { - } - - public static Map load(Path envFile) throws IOException { - Map values = new HashMap<>(); - if (Files.exists(envFile)) { - for (String rawLine : Files.readAllLines(envFile)) { - String line = rawLine.trim(); - if (line.isEmpty() || line.startsWith("#")) { - continue; - } - int idx = line.indexOf('='); - if (idx <= 0) { - continue; - } - String key = line.substring(0, idx).trim(); - String value = line.substring(idx + 1).trim(); - values.put(key, stripQuotes(value)); - } - } - System.getenv().forEach(values::put); - return values; - } - - private static String stripQuotes(String value) { - if (value.length() >= 2) { - char first = value.charAt(0); - char last = value.charAt(value.length() - 1); - if ((first == '"' && last == '"') || (first == '\'' && last == '\'')) { - return value.substring(1, value.length() - 1); - } - } - return value; - } -} diff --git a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/history/HistoryManager.java b/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/history/HistoryManager.java deleted file mode 100644 index f9e132b..0000000 --- a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/history/HistoryManager.java +++ /dev/null @@ -1,162 +0,0 @@ -package shine.agent.botcoder.history; - -import com.fasterxml.jackson.databind.ObjectMapper; -import shine.agent.botcoder.state.RuntimeState; -import shine.agent.botcoder.state.RuntimeStateStore; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.nio.file.StandardOpenOption; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -public class HistoryManager { - - private static final DateTimeFormatter FILE_TS = DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmmss"); - private final Path historyDir; - private final Path archiveDir; - private final RuntimeStateStore stateStore; - private final ObjectMapper mapper; - - public HistoryManager(Path historyDir, Path archiveDir, RuntimeStateStore stateStore) throws IOException { - this.historyDir = historyDir; - this.archiveDir = archiveDir; - this.stateStore = stateStore; - this.mapper = new ObjectMapper(); - Files.createDirectories(historyDir); - Files.createDirectories(archiveDir); - ensureCurrentFile(); - } - - public synchronized Path currentHistoryFile() throws IOException { - return ensureCurrentFile(); - } - - public synchronized Path rotateHistory(String reason, String requestedBy) throws IOException { - Path current = ensureCurrentFile(); - Path archived = archiveDir.resolve(current.getFileName().toString()); - Files.move(current, archived, StandardCopyOption.REPLACE_EXISTING); - Path next = newHistoryFile(); - appendSystemEvent( - "history_rotated", - Map.of( - "reason", reason, - "requestedBy", requestedBy, - "archivedFile", archived.toString() - ) - ); - return next; - } - - public synchronized void appendIncomingText(long chatId, int messageId, String username, String text) throws IOException { - Map payload = basePayload("incoming_text"); - payload.put("chatId", chatId); - payload.put("messageId", messageId); - payload.put("username", username); - payload.put("text", text); - append(payload); - } - - public synchronized void appendIncomingVoice(long chatId, int messageId, String username, String fileId) throws IOException { - Map payload = basePayload("incoming_voice"); - payload.put("chatId", chatId); - payload.put("messageId", messageId); - payload.put("username", username); - payload.put("telegramFileId", fileId); - append(payload); - } - - public synchronized void appendTranscription(String jobId, String text) throws IOException { - Map payload = basePayload("voice_transcription"); - payload.put("jobId", jobId); - payload.put("text", text); - append(payload); - } - - public synchronized void appendCodexRequest(String jobId, String prompt) throws IOException { - Map payload = basePayload("codex_request"); - payload.put("jobId", jobId); - payload.put("prompt", prompt); - append(payload); - } - - public synchronized void appendCodexResponse(String jobId, String response) throws IOException { - Map payload = basePayload("codex_response"); - payload.put("jobId", jobId); - payload.put("response", response); - append(payload); - } - - public synchronized void appendOutgoingMessage(String jobId, long chatId, String text) throws IOException { - Map payload = basePayload("outgoing_message"); - payload.put("jobId", jobId); - payload.put("chatId", chatId); - payload.put("text", text); - append(payload); - } - - public synchronized void appendJobError(String jobId, String error, boolean willRetry, int attempts, int maxRetries) throws IOException { - Map payload = basePayload("job_error"); - payload.put("jobId", jobId); - payload.put("error", error); - payload.put("willRetry", willRetry); - payload.put("attempts", attempts); - payload.put("maxRetries", maxRetries); - append(payload); - } - - public synchronized void appendSystemEvent(String event, Map fields) throws IOException { - Map payload = basePayload(event); - payload.putAll(fields); - append(payload); - } - - private Map basePayload(String type) { - Map payload = new HashMap<>(); - payload.put("timestamp", Instant.now().toString()); - payload.put("type", type); - return payload; - } - - private void append(Map payload) throws IOException { - Path current = ensureCurrentFile(); - Files.writeString( - current, - mapper.writeValueAsString(payload) + System.lineSeparator(), - StandardCharsets.UTF_8, - StandardOpenOption.CREATE, - StandardOpenOption.APPEND - ); - } - - private Path ensureCurrentFile() throws IOException { - RuntimeState snapshot = stateStore.snapshot(); - if (snapshot.currentHistoryFile != null && !snapshot.currentHistoryFile.isBlank()) { - Path configured = Path.of(snapshot.currentHistoryFile); - if (!configured.isAbsolute()) { - configured = historyDir.resolve(snapshot.currentHistoryFile).normalize(); - } - Files.createDirectories(configured.getParent()); - if (!Files.exists(configured)) { - Files.createFile(configured); - } - return configured; - } - return newHistoryFile(); - } - - private Path newHistoryFile() throws IOException { - String name = FILE_TS.format(LocalDateTime.now()) + "_" + UUID.randomUUID().toString().substring(0, 8) + ".jsonl"; - Path file = historyDir.resolve(name); - Files.createFile(file); - stateStore.setCurrentHistoryFile(file.toString()); - return file; - } -} diff --git a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/openai/OpenAiTranscriber.java b/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/openai/OpenAiTranscriber.java deleted file mode 100644 index 60dc0c2..0000000 --- a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/openai/OpenAiTranscriber.java +++ /dev/null @@ -1,81 +0,0 @@ -package shine.agent.botcoder.openai; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.UUID; - -public class OpenAiTranscriber { - - private final HttpClient httpClient; - private final ObjectMapper mapper; - private final String apiKey; - private final String model; - - public OpenAiTranscriber(String apiKey, String model) { - this.apiKey = apiKey; - this.model = model; - this.httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(20)) - .build(); - this.mapper = new ObjectMapper(); - } - - public String transcribe(byte[] audioBytes, String fileName) throws IOException, InterruptedException { - String boundary = "----shine-boundary-" + UUID.randomUUID(); - byte[] body = buildMultipartBody(boundary, audioBytes, fileName); - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("https://api.openai.com/v1/audio/transcriptions")) - .timeout(Duration.ofSeconds(120)) - .header("Authorization", "Bearer " + apiKey) - .header("Content-Type", "multipart/form-data; boundary=" + boundary) - .POST(HttpRequest.BodyPublishers.ofByteArray(body)) - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - if (response.statusCode() >= 300) { - throw new IOException("OpenAI transcription error HTTP " + response.statusCode() + ": " + response.body()); - } - - JsonNode root = mapper.readTree(response.body()); - JsonNode text = root.get("text"); - if (text == null || text.asText().isBlank()) { - throw new IOException("OpenAI transcription returned empty text"); - } - return text.asText().trim(); - } - - private byte[] buildMultipartBody(String boundary, byte[] audioBytes, String fileName) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - String lineEnd = "\r\n"; - String prefix = "--" + boundary + lineEnd; - - out.write(prefix.getBytes(StandardCharsets.UTF_8)); - out.write(("Content-Disposition: form-data; name=\"model\"" + lineEnd + lineEnd).getBytes(StandardCharsets.UTF_8)); - out.write(model.getBytes(StandardCharsets.UTF_8)); - out.write(lineEnd.getBytes(StandardCharsets.UTF_8)); - - out.write(prefix.getBytes(StandardCharsets.UTF_8)); - out.write(("Content-Disposition: form-data; name=\"language\"" + lineEnd + lineEnd).getBytes(StandardCharsets.UTF_8)); - out.write("ru".getBytes(StandardCharsets.UTF_8)); - out.write(lineEnd.getBytes(StandardCharsets.UTF_8)); - - out.write(prefix.getBytes(StandardCharsets.UTF_8)); - out.write(("Content-Disposition: form-data; name=\"file\"; filename=\"" + fileName + "\"" + lineEnd).getBytes(StandardCharsets.UTF_8)); - out.write(("Content-Type: audio/ogg" + lineEnd + lineEnd).getBytes(StandardCharsets.UTF_8)); - out.write(audioBytes); - out.write(lineEnd.getBytes(StandardCharsets.UTF_8)); - - out.write(("--" + boundary + "--" + lineEnd).getBytes(StandardCharsets.UTF_8)); - return out.toByteArray(); - } -} diff --git a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/queue/FailureResult.java b/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/queue/FailureResult.java deleted file mode 100644 index 4a1d368..0000000 --- a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/queue/FailureResult.java +++ /dev/null @@ -1,4 +0,0 @@ -package shine.agent.botcoder.queue; - -public record FailureResult(boolean willRetry, int attempts, int maxRetries) { -} diff --git a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/queue/QueueJob.java b/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/queue/QueueJob.java deleted file mode 100644 index da31b59..0000000 --- a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/queue/QueueJob.java +++ /dev/null @@ -1,54 +0,0 @@ -package shine.agent.botcoder.queue; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import java.time.Instant; -import java.util.UUID; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class QueueJob { - public String id; - public QueueStatus status; - public String type; - public long chatId; - public int messageId; - public String username; - public String text; - public String telegramFileId; - public String historyFile; - public String createdAt; - public String updatedAt; - public String activeSince; - public int attempts; - public String retryReason; - public String lastError; - - public static QueueJob textJob(long chatId, int messageId, String username, String text, String historyFile) { - QueueJob job = baseJob(chatId, messageId, username, historyFile); - job.type = "text"; - job.text = text; - return job; - } - - public static QueueJob voiceJob(long chatId, int messageId, String username, String fileId, String historyFile) { - QueueJob job = baseJob(chatId, messageId, username, historyFile); - job.type = "voice"; - job.telegramFileId = fileId; - return job; - } - - private static QueueJob baseJob(long chatId, int messageId, String username, String historyFile) { - QueueJob job = new QueueJob(); - String now = Instant.now().toString(); - job.id = UUID.randomUUID().toString(); - job.status = QueueStatus.PENDING; - job.chatId = chatId; - job.messageId = messageId; - job.username = username; - job.historyFile = historyFile; - job.createdAt = now; - job.updatedAt = now; - job.attempts = 0; - return job; - } -} diff --git a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/queue/QueueStatus.java b/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/queue/QueueStatus.java deleted file mode 100644 index 91143b5..0000000 --- a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/queue/QueueStatus.java +++ /dev/null @@ -1,6 +0,0 @@ -package shine.agent.botcoder.queue; - -public enum QueueStatus { - PENDING, - ACTIVE -} diff --git a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/queue/QueueStore.java b/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/queue/QueueStore.java deleted file mode 100644 index 779d9dc..0000000 --- a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/queue/QueueStore.java +++ /dev/null @@ -1,203 +0,0 @@ -package shine.agent.botcoder.queue; - -import com.fasterxml.jackson.databind.ObjectMapper; -import shine.agent.botcoder.state.RuntimeStateStore; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Optional; - -public class QueueStore { - - private final Path queueFile; - private final RuntimeStateStore stateStore; - private final ObjectMapper mapper; - private final List jobs; - - public QueueStore(Path queueFile, RuntimeStateStore stateStore) throws IOException { - this.queueFile = queueFile; - this.stateStore = stateStore; - this.mapper = new ObjectMapper(); - Path parent = queueFile.getParent(); - if (parent != null) { - Files.createDirectories(parent); - } - this.jobs = loadQueue(); - persistQueue(); - } - - public synchronized void enqueue(QueueJob job) throws IOException { - jobs.add(job); - persistQueue(); - } - - public synchronized List recoverActiveJobs() throws IOException { - List recovered = new ArrayList<>(); - String now = Instant.now().toString(); - for (QueueJob job : jobs) { - if (job.status == QueueStatus.ACTIVE) { - job.status = QueueStatus.PENDING; - job.retryReason = "service_restart_recovery"; - job.updatedAt = now; - recovered.add(job.id); - } - } - stateStore.setActiveJobId(null); - persistQueue(); - return recovered; - } - - public synchronized Optional activateNext() throws IOException { - for (QueueJob job : jobs) { - if (job.status == QueueStatus.PENDING) { - job.status = QueueStatus.ACTIVE; - job.activeSince = Instant.now().toString(); - job.updatedAt = job.activeSince; - stateStore.setActiveJobId(job.id); - persistQueue(); - return Optional.of(job); - } - } - return Optional.empty(); - } - - public synchronized void markDone(String jobId) throws IOException { - Iterator iterator = jobs.iterator(); - while (iterator.hasNext()) { - QueueJob job = iterator.next(); - if (job.id.equals(jobId)) { - iterator.remove(); - break; - } - } - stateStore.setActiveJobId(null); - persistQueue(); - } - - public synchronized Optional getActiveJob() { - return jobs.stream().filter(j -> j.status == QueueStatus.ACTIVE).findFirst(); - } - - public synchronized int pendingCount() { - int count = 0; - for (QueueJob job : jobs) { - if (job.status == QueueStatus.PENDING) { - count++; - } - } - return count; - } - - public synchronized int totalCount() { - return jobs.size(); - } - - public synchronized List snapshot() { - return new ArrayList<>(jobs); - } - - public synchronized boolean cancelActiveJob(String reason) throws IOException { - for (Iterator iterator = jobs.iterator(); iterator.hasNext(); ) { - QueueJob job = iterator.next(); - if (job.status == QueueStatus.ACTIVE) { - iterator.remove(); - stateStore.setActiveJobId(null); - persistQueue(); - return true; - } - } - return false; - } - - public synchronized int cancelAll(String reason) throws IOException { - int size = jobs.size(); - if (size == 0) { - return 0; - } - jobs.clear(); - stateStore.setActiveJobId(null); - persistQueue(); - return size; - } - - public synchronized boolean cancelByIdPrefix(String idPrefix) throws IOException { - if (idPrefix == null || idPrefix.isBlank()) { - return false; - } - String normalized = idPrefix.trim().toLowerCase(); - for (Iterator iterator = jobs.iterator(); iterator.hasNext(); ) { - QueueJob job = iterator.next(); - if (job.id != null && job.id.toLowerCase().startsWith(normalized)) { - if (job.status == QueueStatus.ACTIVE) { - stateStore.setActiveJobId(null); - } - iterator.remove(); - persistQueue(); - return true; - } - } - return false; - } - - public synchronized FailureResult markFailed(String jobId, String error, int maxRetries) throws IOException { - for (Iterator it = jobs.iterator(); it.hasNext(); ) { - QueueJob job = it.next(); - if (!job.id.equals(jobId)) { - continue; - } - job.attempts = job.attempts + 1; - job.lastError = error; - job.updatedAt = Instant.now().toString(); - stateStore.setActiveJobId(null); - - if (job.attempts < maxRetries) { - job.status = QueueStatus.PENDING; - job.retryReason = error; - persistQueue(); - return new FailureResult(true, job.attempts, maxRetries); - } - - it.remove(); - persistQueue(); - return new FailureResult(false, job.attempts, maxRetries); - } - - stateStore.setActiveJobId(null); - persistQueue(); - return new FailureResult(false, maxRetries, maxRetries); - } - - private List loadQueue() throws IOException { - List loaded = new ArrayList<>(); - if (!Files.exists(queueFile)) { - return loaded; - } - for (String line : Files.readAllLines(queueFile, StandardCharsets.UTF_8)) { - String trimmed = line.trim(); - if (trimmed.isEmpty()) { - continue; - } - loaded.add(mapper.readValue(trimmed, QueueJob.class)); - } - return loaded; - } - - private void persistQueue() throws IOException { - Files.writeString(queueFile, "", StandardCharsets.UTF_8); - for (QueueJob job : jobs) { - Files.writeString( - queueFile, - mapper.writeValueAsString(job) + System.lineSeparator(), - StandardCharsets.UTF_8, - StandardOpenOption.APPEND - ); - } - } -} diff --git a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/state/RuntimeState.java b/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/state/RuntimeState.java deleted file mode 100644 index 5625802..0000000 --- a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/state/RuntimeState.java +++ /dev/null @@ -1,10 +0,0 @@ -package shine.agent.botcoder.state; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class RuntimeState { - public String activeJobId; - public String currentHistoryFile; - public String updatedAt; -} diff --git a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/state/RuntimeStateStore.java b/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/state/RuntimeStateStore.java deleted file mode 100644 index bd3e93b..0000000 --- a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/state/RuntimeStateStore.java +++ /dev/null @@ -1,75 +0,0 @@ -package shine.agent.botcoder.state; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Instant; - -public class RuntimeStateStore { - - private final Path stateFile; - private final ObjectMapper mapper; - private RuntimeState state; - - public RuntimeStateStore(Path stateFile) throws IOException { - this.stateFile = stateFile; - this.mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); - Path parent = stateFile.getParent(); - if (parent != null) { - Files.createDirectories(parent); - } - this.state = loadOrCreate(); - persist(); - } - - public synchronized RuntimeState snapshot() { - RuntimeState copy = new RuntimeState(); - copy.activeJobId = state.activeJobId; - copy.currentHistoryFile = state.currentHistoryFile; - copy.updatedAt = state.updatedAt; - return copy; - } - - public synchronized void setActiveJobId(String activeJobId) throws IOException { - state.activeJobId = activeJobId; - state.updatedAt = Instant.now().toString(); - persist(); - } - - public synchronized void setCurrentHistoryFile(String historyFile) throws IOException { - state.currentHistoryFile = historyFile; - state.updatedAt = Instant.now().toString(); - persist(); - } - - private RuntimeState loadOrCreate() throws IOException { - if (!Files.exists(stateFile)) { - RuntimeState created = new RuntimeState(); - created.updatedAt = Instant.now().toString(); - return created; - } - String raw = Files.readString(stateFile, StandardCharsets.UTF_8).trim(); - if (raw.isEmpty()) { - RuntimeState created = new RuntimeState(); - created.updatedAt = Instant.now().toString(); - return created; - } - RuntimeState loaded = mapper.readValue(raw, RuntimeState.class); - if (loaded.updatedAt == null) { - loaded.updatedAt = Instant.now().toString(); - } - return loaded; - } - - private void persist() throws IOException { - Files.writeString( - stateFile, - mapper.writeValueAsString(state), - StandardCharsets.UTF_8 - ); - } -} diff --git a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/state/SingleInstanceLock.java b/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/state/SingleInstanceLock.java deleted file mode 100644 index 49b9984..0000000 --- a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/state/SingleInstanceLock.java +++ /dev/null @@ -1,50 +0,0 @@ -package shine.agent.botcoder.state; - -import java.io.Closeable; -import java.io.IOException; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; - -public final class SingleInstanceLock implements Closeable { - - private final FileChannel channel; - private final FileLock lock; - private final Path path; - - private SingleInstanceLock(FileChannel channel, FileLock lock, Path path) { - this.channel = channel; - this.lock = lock; - this.path = path; - } - - public static SingleInstanceLock tryAcquire(Path lockFile) throws IOException { - FileChannel channel = FileChannel.open( - lockFile, - StandardOpenOption.CREATE, - StandardOpenOption.WRITE - ); - FileLock lock = channel.tryLock(); - if (lock == null) { - channel.close(); - return null; - } - return new SingleInstanceLock(channel, lock, lockFile); - } - - public Path path() { - return path; - } - - @Override - public void close() throws IOException { - try { - if (lock != null && lock.isValid()) { - lock.release(); - } - } finally { - channel.close(); - } - } -} diff --git a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/telegram/ProcessedUpdatesStore.java b/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/telegram/ProcessedUpdatesStore.java deleted file mode 100644 index e432a28..0000000 --- a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/telegram/ProcessedUpdatesStore.java +++ /dev/null @@ -1,75 +0,0 @@ -package shine.agent.botcoder.telegram; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Iterator; - -public class ProcessedUpdatesStore { - - private final Path file; - private final LinkedHashSet ids = new LinkedHashSet<>(); - private final int maxEntries; - - public ProcessedUpdatesStore(Path file, int maxEntries) throws IOException { - this.file = file; - this.maxEntries = Math.max(100, maxEntries); - Path parent = file.getParent(); - if (parent != null) { - Files.createDirectories(parent); - } - if (Files.exists(file)) { - List lines = Files.readAllLines(file, StandardCharsets.UTF_8); - for (String line : lines) { - String id = line.trim(); - if (!id.isEmpty()) { - ids.add(id); - } - } - } - trimIfNeeded(); - persistAll(); - } - - public synchronized boolean isDuplicateAndMark(String id) throws IOException { - if (id == null || id.isBlank()) { - return false; - } - String normalized = id.trim(); - if (ids.contains(normalized)) { - return true; - } - ids.add(normalized); - trimIfNeeded(); - Files.writeString(file, normalized + System.lineSeparator(), StandardCharsets.UTF_8, - StandardOpenOption.CREATE, StandardOpenOption.APPEND); - return false; - } - - private void trimIfNeeded() throws IOException { - if (ids.size() <= maxEntries) { - return; - } - int toRemove = ids.size() - maxEntries; - int removed = 0; - Iterator it = ids.iterator(); - while (it.hasNext() && removed < toRemove) { - it.next(); - it.remove(); - removed++; - } - persistAll(); - } - - private void persistAll() throws IOException { - StringBuilder sb = new StringBuilder(); - for (String id : ids) { - sb.append(id).append(System.lineSeparator()); - } - Files.writeString(file, sb.toString(), StandardCharsets.UTF_8); - } -} diff --git a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/telegram/ShineAgentBot.java b/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/telegram/ShineAgentBot.java deleted file mode 100644 index 546a77a..0000000 --- a/SHiNE-agent-bot-coder/src/main/java/shine/agent/botcoder/telegram/ShineAgentBot.java +++ /dev/null @@ -1,645 +0,0 @@ -package shine.agent.botcoder.telegram; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.telegram.telegrambots.bots.TelegramLongPollingBot; -import org.telegram.telegrambots.meta.api.methods.GetFile; -import org.telegram.telegrambots.meta.api.methods.send.SendMessage; -import org.telegram.telegrambots.meta.api.objects.Message; -import org.telegram.telegrambots.meta.api.objects.Update; -import org.telegram.telegrambots.meta.api.objects.User; -import org.telegram.telegrambots.meta.exceptions.TelegramApiException; -import shine.agent.botcoder.codex.CodexStatusListener; -import shine.agent.botcoder.codex.CodexClient; -import shine.agent.botcoder.config.AppConfig; -import shine.agent.botcoder.history.HistoryManager; -import shine.agent.botcoder.openai.OpenAiTranscriber; -import shine.agent.botcoder.queue.FailureResult; -import shine.agent.botcoder.queue.QueueJob; -import shine.agent.botcoder.queue.QueueStore; - -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import java.time.Instant; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; - -public class ShineAgentBot extends TelegramLongPollingBot { - - private static final Logger log = LoggerFactory.getLogger(ShineAgentBot.class); - - private final AppConfig config; - private final QueueStore queueStore; - private final HistoryManager historyManager; - private final OpenAiTranscriber transcriber; - private final CodexClient codexClient; - private final ExecutorService worker; - private final ExecutorService notifier; - private final AtomicBoolean running; - private final HttpClient httpClient; - private final ProcessedUpdatesStore processedUpdatesStore; - private final AtomicReference activeJobRef = new AtomicReference<>(); - private final AtomicLong activeJobStartedAt = new AtomicLong(0L); - private final ScheduledExecutorService heartbeatScheduler = Executors.newSingleThreadScheduledExecutor(r -> { - Thread t = new Thread(r, "shine-agent-heartbeat"); - t.setDaemon(true); - return t; - }); - - public ShineAgentBot( - AppConfig config, - QueueStore queueStore, - HistoryManager historyManager, - OpenAiTranscriber transcriber, - CodexClient codexClient, - ProcessedUpdatesStore processedUpdatesStore - ) { - this.config = config; - this.queueStore = queueStore; - this.historyManager = historyManager; - this.transcriber = transcriber; - this.codexClient = codexClient; - this.processedUpdatesStore = processedUpdatesStore; - this.worker = Executors.newSingleThreadExecutor(r -> { - Thread thread = new Thread(r, "shine-agent-bot-worker"); - thread.setDaemon(true); - return thread; - }); - this.notifier = Executors.newSingleThreadExecutor(r -> { - Thread thread = new Thread(r, "shine-agent-bot-notifier"); - thread.setDaemon(true); - return thread; - }); - this.running = new AtomicBoolean(true); - this.httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(20)) - .build(); - } - - public void startWorkers() { - worker.submit(this::processLoop); - } - - public void shutdown() { - running.set(false); - codexClient.stopActiveProcess(); - worker.shutdown(); - notifier.shutdown(); - heartbeatScheduler.shutdownNow(); - try { - if (!worker.awaitTermination(10, TimeUnit.SECONDS)) { - worker.shutdownNow(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - worker.shutdownNow(); - } - try { - if (!notifier.awaitTermination(5, TimeUnit.SECONDS)) { - notifier.shutdownNow(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - notifier.shutdownNow(); - } - } - - @Override - public String getBotUsername() { - return config.botUsername(); - } - - @Override - public String getBotToken() { - return config.telegramBotToken(); - } - - @Override - public void onUpdateReceived(Update update) { - if (update == null || !update.hasMessage()) { - return; - } - Message message = update.getMessage(); - try { - String updateId = message.getChatId() + ":" + message.getMessageId(); - if (processedUpdatesStore.isDuplicateAndMark(updateId)) { - log.info("Дубликат update пропущен: {}", updateId); - return; - } - } catch (IOException e) { - log.error("Не удалось проверить дубликат update", e); - } - - User from = message.getFrom(); - if (from == null) { - return; - } - String username = AppConfig.normalizeUsername(from.getUserName()); - if (!username.equals(config.allowedTelegramUsername())) { - return; - } - - try { - if (message.hasText() && "/new".equalsIgnoreCase(message.getText().trim())) { - handleNewCommand(message, username); - return; - } - if (message.hasText() && handleControlCommand(message)) { - return; - } - if (message.hasText() && !message.getText().isBlank()) { - enqueueText(message, username); - return; - } - if (message.hasVoice()) { - enqueueVoice(message, username); - return; - } - if (message.hasAudio()) { - enqueueAudio(message, username); - } - } catch (Exception e) { - log.error("Ошибка обработки update", e); - safeSendText(message.getChatId(), "Ошибка обработки входящего сообщения: " + shortError(e), message.getMessageId()); - } - } - - private void handleNewCommand(Message message, String username) throws IOException { - historyManager.appendSystemEvent( - "command_new_received", - Map.of( - "chatId", message.getChatId(), - "messageId", message.getMessageId(), - "username", username - ) - ); - var archived = historyManager.rotateHistory("command_new", username); - String response = "История очищена. Новый диалог начат.\nАрхив: " + archived.getFileName(); - safeSendText(message.getChatId(), response, message.getMessageId()); - } - - private void enqueueText(Message message, String username) throws IOException { - QueueJob job = QueueJob.textJob( - message.getChatId(), - message.getMessageId(), - username, - message.getText(), - historyManager.currentHistoryFile().toString() - ); - historyManager.appendIncomingText(message.getChatId(), message.getMessageId(), username, message.getText()); - queueStore.enqueue(job); - safeSendText(message.getChatId(), "Принял в очередь: " + shortId(job.id), message.getMessageId()); - } - - private void enqueueVoice(Message message, String username) throws IOException { - String fileId = message.getVoice().getFileId(); - QueueJob job = QueueJob.voiceJob( - message.getChatId(), - message.getMessageId(), - username, - fileId, - historyManager.currentHistoryFile().toString() - ); - historyManager.appendIncomingVoice(message.getChatId(), message.getMessageId(), username, fileId); - queueStore.enqueue(job); - safeSendText(message.getChatId(), "Голосовое принято в очередь: " + shortId(job.id), message.getMessageId()); - } - - private void enqueueAudio(Message message, String username) throws IOException { - String fileId = message.getAudio().getFileId(); - QueueJob job = QueueJob.voiceJob( - message.getChatId(), - message.getMessageId(), - username, - fileId, - historyManager.currentHistoryFile().toString() - ); - historyManager.appendIncomingVoice(message.getChatId(), message.getMessageId(), username, fileId); - queueStore.enqueue(job); - safeSendText(message.getChatId(), "Аудио принято в очередь: " + shortId(job.id), message.getMessageId()); - } - - private void processLoop() { - while (running.get()) { - try { - Optional next = queueStore.activateNext(); - if (next.isEmpty()) { - Thread.sleep(500); - continue; - } - processJob(next.get()); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - return; - } catch (Exception e) { - log.error("Ошибка worker loop", e); - try { - Thread.sleep(1000); - } catch (InterruptedException interrupted) { - Thread.currentThread().interrupt(); - return; - } - } - } - } - - private void processJob(QueueJob job) { - ScheduledFuture heartbeat = null; - AtomicLong lastJobNotificationAt = new AtomicLong(0L); - try { - log.info("Начало обработки jobId={}, type={}, chatId={}, attempts={}", job.id, job.type, job.chatId, job.attempts); - activeJobRef.set(job); - long startedAt = System.currentTimeMillis(); - activeJobStartedAt.set(startedAt); - safeSendText(job.chatId, "Задача " + shortId(job.id) + " взята в работу.", job.messageId); - lastJobNotificationAt.set(startedAt); - String userText = resolveUserText(job); - String prompt = buildPrompt(job, userText); - historyManager.appendCodexRequest(job.id, prompt); - - log.info("Вызов Codex для jobId={}", job.id); - heartbeat = heartbeatScheduler.scheduleAtFixedRate( - () -> { - long now = System.currentTimeMillis(); - long elapsed = now - startedAt; - long silence = now - lastJobNotificationAt.get(); - if (elapsed >= 120_000L && silence >= 120_000L) { - lastJobNotificationAt.set(now); - notifier.submit(() -> safeSendText( - job.chatId, - "Статус " + shortId(job.id) + ": задача ещё выполняется, работает уже " - + formatDuration(elapsedSeconds()) - + ". От Codex давно нет сообщений.", - job.messageId - )); - } - }, - 120, 10, TimeUnit.SECONDS - ); - String answer; - answer = codexClient.executePrompt(prompt, buildStatusListener(job, lastJobNotificationAt)); - log.info("Codex завершился для jobId={}, длина ответа={}", job.id, answer.length()); - safeSendText(job.chatId, "Codex завершил обработку, отправляю результат.", job.messageId); - sendLongMessage(job.chatId, answer, job.messageId); - historyManager.appendCodexResponse(job.id, answer); - historyManager.appendOutgoingMessage(job.id, job.chatId, answer); - queueStore.markDone(job.id); - log.info("Задача завершена jobId={}", job.id); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - handleInterruptedJob(job, e); - } catch (Exception e) { - handleFailedJob(job, e); - } finally { - if (heartbeat != null) { - heartbeat.cancel(true); - } - activeJobRef.set(null); - activeJobStartedAt.set(0L); - } - } - - private void handleInterruptedJob(QueueJob job, InterruptedException e) { - if (!running.get()) { - log.info("Обработка jobId={} прервана из-за остановки сервиса", job.id); - try { - historyManager.appendSystemEvent("job_interrupted_on_shutdown", Map.of( - "jobId", job.id, - "reason", shortError(e) - )); - } catch (Exception ignored) { - } - return; - } - handleFailedJob(job, e); - } - - private void handleFailedJob(QueueJob job, Exception e) { - String error = shortError(e); - log.error("Ошибка обработки jobId={}: {}", job.id, error, e); - try { - if (!isJobStillInQueue(job.id)) { - log.info("Задача {} уже удалена из очереди, ошибка не ретраится", job.id); - return; - } - FailureResult failure = queueStore.markFailed(job.id, error, config.maxRetries()); - historyManager.appendJobError(job.id, error, failure.willRetry(), failure.attempts(), failure.maxRetries()); - String message = failure.willRetry() - ? "Ошибка выполнения задачи " + shortId(job.id) + ", повтор: " + failure.attempts() + "/" + failure.maxRetries() - : "Ошибка выполнения задачи " + shortId(job.id) + ". Лимит попыток исчерпан."; - safeSendText(job.chatId, message, job.messageId); - } catch (Exception inner) { - log.error("Не удалось зафиксировать ошибку задачи {}", job.id, inner); - } - } - - private boolean isJobStillInQueue(String jobId) { - for (QueueJob item : queueStore.snapshot()) { - if (jobId.equals(item.id)) { - return true; - } - } - return false; - } - - private CodexStatusListener buildStatusListener(QueueJob job, AtomicLong lastJobNotificationAt) { - AtomicReference lastStatus = new AtomicReference<>(""); - AtomicLong lastSentAt = new AtomicLong(0L); - return status -> { - long now = System.currentTimeMillis(); - String prev = lastStatus.get(); - boolean changed = !status.equals(prev); - if (changed && now - lastSentAt.get() > 8_000) { - String text = "Статус " + shortId(job.id) + ": " + status; - notifier.submit(() -> safeSendText(job.chatId, text, job.messageId)); - lastStatus.set(status); - lastSentAt.set(now); - lastJobNotificationAt.set(now); - } - }; - } - - private String resolveUserText(QueueJob job) throws IOException, InterruptedException, TelegramApiException { - if (!"voice".equals(job.type)) { - return job.text; - } - byte[] audio = downloadTelegramFile(job.telegramFileId); - String transcription = transcriber.transcribe(audio, job.id + ".ogg"); - historyManager.appendTranscription(job.id, transcription); - safeSendText(job.chatId, "Распознано:\n" + transcription, job.messageId); - return transcription; - } - - private byte[] downloadTelegramFile(String fileId) throws IOException, InterruptedException, TelegramApiException { - GetFile getFile = new GetFile(fileId); - org.telegram.telegrambots.meta.api.objects.File tgFile = execute(getFile); - String fileUrl = tgFile.getFileUrl(getBotToken()); - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(fileUrl)) - .timeout(Duration.ofSeconds(60)) - .GET() - .build(); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); - if (response.statusCode() >= 300) { - throw new IOException("Telegram file download failed HTTP " + response.statusCode()); - } - return response.body(); - } - - private String buildPrompt(QueueJob job, String text) { - String retryBlock = ""; - if (job.retryReason != null && !job.retryReason.isBlank()) { - retryBlock = "\n\nПометка retry: " + job.retryReason; - } - - return """ - Пришло сообщение в Telegram. - Тип: %s - Username отправителя: @%s - Текст для обработки: - %s - - История диалога (JSONL): %s - Инструкции агента: %s - Работай в рабочем проекте аккуратно и верни только текст ответа пользователю.%s - """.formatted( - job.type, - job.username, - text, - job.historyFile, - config.agentInstructionsFile(), - retryBlock - ); - } - - private boolean handleControlCommand(Message message) throws IOException { - String text = message.getText().trim(); - String lower = text.toLowerCase(); - - if ("/start".equals(lower) || "/help".equals(lower)) { - safeSendText(message.getChatId(), helpText(), message.getMessageId()); - return true; - } - if ("/status".equals(lower)) { - safeSendText(message.getChatId(), buildStatusText(), message.getMessageId()); - return true; - } - if ("/queue".equals(lower)) { - safeSendText(message.getChatId(), buildQueueText(), message.getMessageId()); - return true; - } - if ("/restart_service".equals(lower) || "/restart".equals(lower)) { - historyManager.appendSystemEvent("restart_service_requested", Map.of( - "chatId", message.getChatId(), - "messageId", message.getMessageId(), - "timestamp", Instant.now().toString() - )); - safeSendText( - message.getChatId(), - "Перезапускаю сервис. Если задача была активна, после старта она вернётся в очередь и продолжится.", - message.getMessageId() - ); - scheduleSelfRestart(); - return true; - } - if ("/stop".equals(lower)) { - boolean stopped = codexClient.stopActiveProcess(); - if (stopped) { - queueStore.cancelActiveJob("stopped_by_user"); - historyManager.appendSystemEvent("job_stopped_by_user", Map.of( - "timestamp", Instant.now().toString() - )); - safeSendText(message.getChatId(), "Текущая задача остановлена и удалена из очереди.", message.getMessageId()); - } else { - safeSendText(message.getChatId(), "Сейчас нет активной задачи.", message.getMessageId()); - } - return true; - } - if (lower.startsWith("/cancel")) { - String[] parts = text.split("\\s+", 2); - if (parts.length < 2) { - safeSendText(message.getChatId(), "Использование: /cancel ", message.getMessageId()); - return true; - } - String arg = parts[1].trim(); - if ("all".equalsIgnoreCase(arg)) { - codexClient.stopActiveProcess(); - int cancelled = queueStore.cancelAll("cancel_all_by_user"); - safeSendText(message.getChatId(), "Удалено задач из очереди: " + cancelled, message.getMessageId()); - return true; - } - Optional active = queueStore.getActiveJob(); - if (active.isPresent() && active.get().id != null - && active.get().id.toLowerCase().startsWith(arg.toLowerCase())) { - codexClient.stopActiveProcess(); - } - boolean cancelled = queueStore.cancelByIdPrefix(arg); - safeSendText(message.getChatId(), - cancelled ? "Задача удалена: " + arg : "Задача не найдена: " + arg, - message.getMessageId()); - return true; - } - return false; - } - - private String buildStatusText() { - Optional active = queueStore.getActiveJob(); - int pending = queueStore.pendingCount(); - if (active.isEmpty()) { - return "Статус: активной задачи нет.\nВ очереди pending: " + pending; - } - QueueJob job = active.get(); - return "Статус: активная задача " + shortId(job.id) + - "\nТип: " + job.type + - "\nПопытка: " + (job.attempts + 1) + "/" + config.maxRetries() + - "\nВыполняется: " + elapsedSeconds() + "с" + - "\nPending: " + pending; - } - - private String buildQueueText() { - List jobs = queueStore.snapshot(); - if (jobs.isEmpty()) { - return "Очередь пуста."; - } - StringBuilder sb = new StringBuilder(); - sb.append("Очередь: ").append(jobs.size()).append('\n'); - int limit = Math.min(jobs.size(), 10); - for (int i = 0; i < limit; i++) { - QueueJob job = jobs.get(i); - sb.append(i + 1).append(") ") - .append(shortId(job.id)) - .append(" [").append(job.status).append("] ") - .append(job.type) - .append(" attempts=").append(job.attempts) - .append('\n'); - } - if (jobs.size() > limit) { - sb.append("...и ещё ").append(jobs.size() - limit).append(" задач"); - } - return sb.toString().trim(); - } - - private String helpText() { - return """ - Доступные команды: - /status — активная задача и размер очереди - /queue — список задач в очереди - /stop — остановить текущую задачу - /cancel — удалить задачу по id (префикс) или все - /new — архивировать историю и начать новую - /restart_service — перезапустить сервис через systemd - /help — эта справка - """; - } - - private void scheduleSelfRestart() { - Thread restartThread = new Thread(() -> { - try { - Thread.sleep(1500); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - log.info("Перезапуск сервиса запрошен командой Telegram"); - System.exit(0); - }, "shine-agent-self-restart"); - restartThread.setDaemon(true); - restartThread.start(); - } - - private void safeSendText(long chatId, String text, Integer replyToMessageId) { - try { - SendMessage message = new SendMessage(); - message.setChatId(String.valueOf(chatId)); - message.setText(trimForTelegram(text)); - if (replyToMessageId != null) { - message.setReplyToMessageId(replyToMessageId); - } - execute(message); - } catch (Exception e) { - log.error("Не удалось отправить сообщение в Telegram", e); - } - } - - private void sendLongMessage(long chatId, String text, Integer replyToMessageId) { - String normalized = text == null ? "" : text.strip(); - if (normalized.isEmpty()) { - safeSendText(chatId, "(пустой ответ)", replyToMessageId); - return; - } - int max = 3500; - int start = 0; - while (start < normalized.length()) { - int end = Math.min(start + max, normalized.length()); - String part = normalized.substring(start, end); - safeSendText(chatId, part, replyToMessageId); - start = end; - } - } - - private String trimForTelegram(String value) { - if (value == null) { - return ""; - } - String text = value.strip(); - int max = 3900; - if (text.length() <= max) { - return text; - } - return text.substring(0, max) + "\n...[обрезано]"; - } - - private String shortId(String id) { - if (id == null || id.length() < 8) { - return id; - } - return id.substring(0, 8); - } - - private long elapsedSeconds() { - long started = activeJobStartedAt.get(); - if (started <= 0) { - return 0; - } - return (System.currentTimeMillis() - started) / 1000L; - } - - private String formatDuration(long seconds) { - long safeSeconds = Math.max(0, seconds); - long hours = safeSeconds / 3600; - long minutes = (safeSeconds % 3600) / 60; - long sec = safeSeconds % 60; - if (hours > 0) { - return hours + "ч " + minutes + "м " + sec + "с"; - } - if (minutes > 0) { - return minutes + "м " + sec + "с"; - } - return sec + "с"; - } - - private String shortError(Throwable e) { - String message = e.getMessage(); - if (message == null || message.isBlank()) { - return e.getClass().getSimpleName(); - } - String normalized = message.replace('\n', ' ').replace('\r', ' ').trim(); - if (normalized.length() > 600) { - return normalized.substring(0, 600); - } - return normalized; - } -} diff --git a/VERSION.properties b/VERSION.properties index a964d9e..b7fb5a7 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.86 -server.version=1.2.80 +client.version=1.2.87 +server.version=1.2.81 diff --git a/settings.gradle b/settings.gradle index 31ab4f8..1a7ab1f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,4 +8,3 @@ include 'shine-server-blockchain' include 'shine-server-db' include 'shine-server-net-protocol' include 'shine-server-net-server' -include 'SHiNE-agent-bot-coder'