SHA256
Обновить Telegram-бота, документацию и связанные доработки
This commit is contained in:
@@ -34,8 +34,10 @@
|
||||
- Для приватных voice/audio-запросов в публичном отчёте первым сообщением отправлять исходный Telegram voice/audio-файл с подписью, где указан распознанный текст. В пользовательском тексте отчёта не показывать Telegram `file_id`.
|
||||
- Озвучивание финальных ответов настраивается персонально для каждого Telegram-пользователя командами `/voice_on`, `/voice_off`; для новых пользователей оно включено по умолчанию.
|
||||
- Адаптация текста перед озвучкой настраивается персонально командами `/voice_rewrite_on`, `/voice_rewrite_off`. Если она включена, сервис перед TTS вызывает дешёвую текстовую модель OpenAI и делает голосовую версию без длинных хэшей, путей, команд и технического шума, сохраняя смысл и порядок исходного ответа.
|
||||
- Режим личных ответов настраивается персонально командами `/single_message_on`, `/single_message_off`: либо одно редактируемое сообщение по этапам, либо отдельные сообщения как раньше.
|
||||
- Команда `/settings` должна сразу показывать текущее состояние всех персональных настроек пользователя и список команд для их изменения.
|
||||
- Если озвучивание включено, после полного текстового финального ответа сервис дополнительно отправляет voice-файл с синтезированной речью через OpenAI TTS даже для текстовых запросов. Voice отправляется в исходный чат, а также в известный личный чат пользователя и в общий чат `@shine_writing`, если они отличаются и доступны. Промежуточные статусы не озвучивать.
|
||||
- Команда `/status` должна показывать состояние очереди и персональные состояния голосовых функций: включены ли voice-ответы и адаптация текста перед озвучкой.
|
||||
- Команда `/status` должна показывать состояние очереди и персональные настройки: voice-ответы, адаптацию текста перед озвучкой и режим одного сообщения в личке.
|
||||
|
||||
## Правила голосовой версии ответа
|
||||
- Текстовый финальный ответ должен оставаться полноценным: в нём можно указывать команды, пути, хэши коммитов, номера версий, результаты проверок и другие технические детали.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
- обрабатывает задачи строго последовательно;
|
||||
- поддерживает текстовые и голосовые сообщения (voice/audio через OpenAI transcription);
|
||||
- вызывает Codex CLI и отправляет ответ в Telegram;
|
||||
- в личном чате умеет работать в двух персонально переключаемых режимах: через одно редактируемое статусное сообщение или через отдельные сообщения по этапам;
|
||||
- умеет персонально для каждого пользователя озвучивать финальный ответ через OpenAI TTS;
|
||||
- при рестарте восстанавливает незавершённые задачи;
|
||||
- отправляет аварийный статус только если Codex молчит 2 минуты подряд во время активной задачи;
|
||||
@@ -60,6 +61,13 @@ python3 SHiNE-agent-bot-coder/py_bot_service.py --selftest-codex "Ответь
|
||||
- Если Telegram заранее сообщает большой размер файла, бот больше не отказывается сразу: сначала явно пишет, что пробует скачать файл, затем отдельно сообщает, удалось ли скачивание, и только после успешной загрузки переходит к подготовке аудио и OpenAI.
|
||||
- Для очень больших файлов упираемся не только в OpenAI, но и в лимит обычного облачного Telegram Bot API на скачивание файла ботом. Для таких случаев нужно использовать локальный `telegram-bot-api` сервер и указать его через `TELEGRAM_API_BASE_URL`.
|
||||
|
||||
## Статусы в личке
|
||||
- Для `private`-чата бот поддерживает персональную настройку режима ответа.
|
||||
- По умолчанию он старается не засорять переписку промежуточными сообщениями: создаёт одно статусное сообщение и редактирует его по этапам.
|
||||
- Если включить `/single_message_off`, бот возвращается к старому режиму и отправляет отдельные сообщения по этапам и финальный ответ отдельно.
|
||||
- Если финальный текст в режиме одного сообщения не помещается целиком, бот оставляет первую часть в отредактированном статусном сообщении и отправляет максимум ещё одно дополнительное текстовое сообщение с хвостом ответа.
|
||||
- Голосовой ответ, если он включён, всегда приходит отдельным новым сообщением.
|
||||
|
||||
## Запуск как systemd-сервис
|
||||
Файлы для установки:
|
||||
- `scripts/systemd/shine-agent-bot-coder.service`
|
||||
@@ -77,6 +85,7 @@ python3 SHiNE-agent-bot-coder/py_bot_service.py --selftest-codex "Ответь
|
||||
|
||||
## Telegram-команды
|
||||
- `/status` — активная задача и размер очереди.
|
||||
- `/settings` — текущие пользовательские настройки и команды для их изменения.
|
||||
- `/queue` — список задач в очереди.
|
||||
- `/stop` — остановить текущую задачу.
|
||||
- `/cancel <id|all>` — удалить задачу по id/префиксу или очистить очередь.
|
||||
@@ -85,5 +94,7 @@ python3 SHiNE-agent-bot-coder/py_bot_service.py --selftest-codex "Ответь
|
||||
- `/voice_off` — выключить озвучивание финальных ответов для текущего пользователя.
|
||||
- `/voice_rewrite_on` — включить адаптацию текста перед озвучкой.
|
||||
- `/voice_rewrite_off` — выключить адаптацию текста перед озвучкой.
|
||||
- `/single_message_on` — вести ответ в личке через одно редактируемое сообщение.
|
||||
- `/single_message_off` — слать отдельные сообщения по этапам и отдельный финальный ответ.
|
||||
- `/restart` или `/restart_service` — отложенный рестарт после текущей задачи, до взятия следующей (только для Айдара).
|
||||
- `/restart_hard` — жёсткий рестарт прямо сейчас (только для Айдара).
|
||||
|
||||
@@ -77,6 +77,23 @@ def split_long_text(text: str, chunk_size: int = 3500) -> list[str]:
|
||||
return [text[i:i + chunk_size] for i in range(0, len(text), chunk_size)]
|
||||
|
||||
|
||||
def split_final_private_text(text: str, first_chunk_size: int = 3900, second_chunk_size: int = 3900) -> list[str]:
|
||||
text = (text or "").strip()
|
||||
if not text:
|
||||
return ["(пустой ответ)"]
|
||||
if len(text) <= first_chunk_size:
|
||||
return [text]
|
||||
first = text[:first_chunk_size].rstrip()
|
||||
rest = text[first_chunk_size:].lstrip()
|
||||
if len(rest) <= second_chunk_size:
|
||||
return [first, rest]
|
||||
second = rest[:second_chunk_size].rstrip()
|
||||
if len(second) > 40:
|
||||
second = second[:-40].rstrip()
|
||||
second = second.rstrip() + "\n...[ответ обрезан]"
|
||||
return [first, second]
|
||||
|
||||
|
||||
def split_text_for_tts(text: str, chunk_size: int) -> list[str]:
|
||||
text = (text or "").strip()
|
||||
if not text:
|
||||
@@ -266,6 +283,10 @@ class TelegramApi:
|
||||
payload["reply_to_message_id"] = reply_to_message_id
|
||||
return self.call("sendMessage", payload=payload, timeout=30)
|
||||
|
||||
def edit_message_text(self, chat_id: int | str, message_id: int, text: str) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {"chat_id": chat_id, "message_id": message_id, "text": text}
|
||||
return self.call("editMessageText", payload=payload, timeout=30)
|
||||
|
||||
def send_voice(
|
||||
self,
|
||||
chat_id: int | str,
|
||||
@@ -690,6 +711,8 @@ class ShinePyBotService:
|
||||
user_settings["voice_replies_enabled"] = True
|
||||
if not isinstance(user_settings.get("voice_rewrite_enabled"), bool):
|
||||
user_settings["voice_rewrite_enabled"] = True
|
||||
if not isinstance(user_settings.get("single_status_message_enabled"), bool):
|
||||
user_settings["single_status_message_enabled"] = True
|
||||
return user_settings
|
||||
|
||||
def _voice_replies_enabled(self, username: str) -> bool:
|
||||
@@ -706,6 +729,13 @@ class ShinePyBotService:
|
||||
self._user_settings(username)["voice_rewrite_enabled"] = enabled
|
||||
self._persist_state()
|
||||
|
||||
def _single_status_message_enabled(self, username: str) -> bool:
|
||||
return bool(self._user_settings(username).get("single_status_message_enabled"))
|
||||
|
||||
def _set_single_status_message_enabled(self, username: str, enabled: bool) -> None:
|
||||
self._user_settings(username)["single_status_message_enabled"] = enabled
|
||||
self._persist_state()
|
||||
|
||||
def _remember_private_chat(self, username: str, chat_id: int) -> None:
|
||||
uname = normalize_username(username)
|
||||
if not uname:
|
||||
@@ -1088,7 +1118,15 @@ class ShinePyBotService:
|
||||
with self.queue_lock:
|
||||
self.queue.append(job)
|
||||
self._persist_queue()
|
||||
self._safe_send(chat_id, f"Принял задачу #{job['num']}", reply_to=message_id)
|
||||
if chat_type == "private":
|
||||
self._ensure_job_status_message(
|
||||
job["id"],
|
||||
chat_id,
|
||||
message_id,
|
||||
f"Задача #{job['num']} получена.\nСтатус: в очереди.",
|
||||
)
|
||||
else:
|
||||
self._safe_send(chat_id, f"Принял задачу #{job['num']}", reply_to=message_id)
|
||||
|
||||
def _enqueue_voice_job(
|
||||
self,
|
||||
@@ -1140,7 +1178,15 @@ class ShinePyBotService:
|
||||
with self.queue_lock:
|
||||
self.queue.append(job)
|
||||
self._persist_queue()
|
||||
self._safe_send(chat_id, f"Принял voice в задачу #{job['num']}", reply_to=message_id)
|
||||
if chat_type == "private":
|
||||
self._ensure_job_status_message(
|
||||
job["id"],
|
||||
chat_id,
|
||||
message_id,
|
||||
f"Voice для задачи #{job['num']} получен.\nСтатус: в очереди.",
|
||||
)
|
||||
else:
|
||||
self._safe_send(chat_id, f"Принял voice в задачу #{job['num']}", reply_to=message_id)
|
||||
|
||||
def _build_job_base(self, chat_id: int, message_id: int, username: str, history_file: str) -> dict[str, Any]:
|
||||
with self.queue_lock:
|
||||
@@ -1172,6 +1218,8 @@ class ShinePyBotService:
|
||||
"created_at": now_iso(),
|
||||
"updated_at": now_iso(),
|
||||
"active_since": None,
|
||||
"status_message_id": None,
|
||||
"status_message_text": "",
|
||||
}
|
||||
|
||||
def _handle_task_center_text(self, chat_id: int, message_id: int, username: str, text: str) -> bool:
|
||||
@@ -1279,6 +1327,9 @@ class ShinePyBotService:
|
||||
if command in ("/start", "/help"):
|
||||
self._safe_send(chat_id, self._help_text(is_owner=is_owner), reply_to=message_id)
|
||||
return
|
||||
if command == "/settings":
|
||||
self._safe_send(chat_id, self._settings_text(username), reply_to=message_id)
|
||||
return
|
||||
if command == "/status":
|
||||
self._safe_send(chat_id, self._status_text(username), reply_to=message_id)
|
||||
return
|
||||
@@ -1317,6 +1368,16 @@ class ShinePyBotService:
|
||||
self._append_history_event("voice_rewrite_disabled", {"username": normalize_username(username)}, username=username)
|
||||
self._safe_send(chat_id, "Адаптация текста перед озвучкой выключена для вашего пользователя.", reply_to=message_id)
|
||||
return
|
||||
if command == "/single_message_on":
|
||||
self._set_single_status_message_enabled(username, True)
|
||||
self._append_history_event("single_status_message_enabled", {"username": normalize_username(username)}, username=username)
|
||||
self._safe_send(chat_id, "Режим одного редактируемого сообщения в личке включён для вашего пользователя.", reply_to=message_id)
|
||||
return
|
||||
if command == "/single_message_off":
|
||||
self._set_single_status_message_enabled(username, False)
|
||||
self._append_history_event("single_status_message_disabled", {"username": normalize_username(username)}, username=username)
|
||||
self._safe_send(chat_id, "Режим одного редактируемого сообщения в личке выключен. Бот будет отправлять отдельные сообщения по этапам.", reply_to=message_id)
|
||||
return
|
||||
if command == "/new":
|
||||
archived = self._rotate_history("command_new", username)
|
||||
self._safe_send(chat_id, f"История очищена. Новый диалог начат.\nАрхив: {archived.name}", reply_to=message_id)
|
||||
@@ -1383,15 +1444,12 @@ class ShinePyBotService:
|
||||
lines = [
|
||||
"Доступные команды:",
|
||||
"/status — активная задача и размер очереди",
|
||||
"/settings — текущие настройки и команды для их изменения",
|
||||
"/queue — список задач в очереди",
|
||||
"/tasks — список ваших задач и предложений",
|
||||
"/stop — остановить текущую задачу",
|
||||
"/cancel <id|all> — удалить задачу по id (префикс) или все",
|
||||
"/new — архивировать историю и начать новую",
|
||||
"/voice_on — включить озвучивание финальных ответов",
|
||||
"/voice_off — выключить озвучивание финальных ответов",
|
||||
"/voice_rewrite_on — включить адаптацию текста перед озвучкой",
|
||||
"/voice_rewrite_off — выключить адаптацию текста перед озвучкой",
|
||||
"/help — эта справка",
|
||||
]
|
||||
if is_owner:
|
||||
@@ -1400,15 +1458,37 @@ class ShinePyBotService:
|
||||
lines.insert(-1, "/restart_hard — жёсткий рестарт прямо сейчас")
|
||||
return "\n".join(lines)
|
||||
|
||||
def _settings_text(self, username: str) -> str:
|
||||
voice_status = "включено" if self._voice_replies_enabled(username) else "выключено"
|
||||
rewrite_status = "включена" if self._voice_rewrite_enabled(username) else "выключена"
|
||||
single_message_status = "включён" if self._single_status_message_enabled(username) else "выключен"
|
||||
lines = [
|
||||
"Текущие настройки:",
|
||||
f"Озвучивание финальных ответов: {voice_status}",
|
||||
f"Адаптация текста перед озвучкой: {rewrite_status}",
|
||||
f"Режим одного редактируемого сообщения в личке: {single_message_status}",
|
||||
"",
|
||||
"Команды настроек:",
|
||||
"/voice_on — включить озвучивание",
|
||||
"/voice_off — выключить озвучивание",
|
||||
"/voice_rewrite_on — адаптировать текст перед озвучкой",
|
||||
"/voice_rewrite_off — озвучивать обычный текст без адаптации",
|
||||
"/single_message_on — один редактируемый ответ в личке",
|
||||
"/single_message_off — отдельные сообщения по этапам и финалу",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
def _status_text(self, username: str) -> str:
|
||||
with self.queue_lock:
|
||||
active = next((j for j in self.queue if j.get("status") == "active"), None)
|
||||
pending = sum(1 for j in self.queue if j.get("status") == "pending")
|
||||
voice_status = "включено" if self._voice_replies_enabled(username) else "выключено"
|
||||
rewrite_status = "включена" if self._voice_rewrite_enabled(username) else "выключена"
|
||||
single_message_status = "включён" if self._single_status_message_enabled(username) else "выключен"
|
||||
settings_text = (
|
||||
f"Голосовые ответы: {voice_status}\n"
|
||||
f"Адаптация текста перед озвучкой: {rewrite_status}"
|
||||
f"Адаптация текста перед озвучкой: {rewrite_status}\n"
|
||||
f"Режим одного сообщения в личке: {single_message_status}"
|
||||
)
|
||||
restart_text = "\nОтложенный рестарт: ожидает завершения текущей задачи" if self.restart_requested else ""
|
||||
if not active:
|
||||
@@ -1505,38 +1585,53 @@ class ShinePyBotService:
|
||||
chat_id = int(job["chat_id"])
|
||||
message_id = int(job["message_id"])
|
||||
history_path = Path(job["history_file"])
|
||||
self._safe_send(chat_id, f"Задача #{job_num} в работе.", reply_to=message_id)
|
||||
private_single_message = (
|
||||
(job.get("chat_type") or "") == "private"
|
||||
and self._single_status_message_enabled(job.get("username") or "")
|
||||
)
|
||||
self._set_job_status_text(job, f"Задача #{job_num} в работе.\nСтатус: выполняется.")
|
||||
try:
|
||||
if job.get("type") == "voice":
|
||||
self._safe_send(chat_id, f"#{job_num}: распознаю голосовое...", reply_to=message_id)
|
||||
self._set_job_status_text(job, f"Задача #{job_num} в работе.\nСтатус: распознаю voice.")
|
||||
recognized = self._transcribe_voice_job(
|
||||
job,
|
||||
status_cb=lambda note: self._safe_send(chat_id, f"#{job_num}: {note}", reply_to=message_id),
|
||||
status_cb=lambda note: self._set_job_status_text(
|
||||
job,
|
||||
f"Задача #{job_num} в работе.\nСтатус: {note}",
|
||||
),
|
||||
)
|
||||
job["text"] = recognized
|
||||
self._append_history(history_path, "voice_transcription", {"jobId": job_id, "jobNum": job_num, "text": recognized})
|
||||
preview = recognized.strip()
|
||||
if len(preview) > 1200:
|
||||
preview = preview[:1200] + " ...[обрезано]"
|
||||
self._safe_send(chat_id, f"#{job_num}: распознано:\n{preview}", reply_to=message_id)
|
||||
self._safe_send(chat_id, f"#{job_num}: распознано, отправляю в Codex.", reply_to=message_id)
|
||||
if len(preview) > 800:
|
||||
preview = preview[:800].rstrip() + " ...[обрезано]"
|
||||
self._set_job_status_text(
|
||||
job,
|
||||
f"Задача #{job_num} в работе.\nСтатус: voice распознан, отправляю в Codex.\n\nТекст:\n{preview}",
|
||||
)
|
||||
|
||||
prompt = self._build_prompt(job)
|
||||
self._append_history(history_path, "codex_request", {"jobId": job_id, "prompt": prompt})
|
||||
answer = self._run_codex(prompt, chat_id, message_id, job_id, job_num)
|
||||
for chunk in split_long_text(answer):
|
||||
self._safe_send(chat_id, chunk, reply_to=message_id)
|
||||
self._set_job_status_text(job, f"Задача #{job_num} в работе.\nСтатус: выполняю через Codex.")
|
||||
answer = self._run_codex(prompt, job)
|
||||
if private_single_message:
|
||||
parts = split_final_private_text(answer)
|
||||
self._set_job_status_text(job, parts[0])
|
||||
if len(parts) > 1:
|
||||
self._safe_send(chat_id, parts[1], reply_to=message_id)
|
||||
else:
|
||||
for chunk in split_long_text(answer):
|
||||
self._safe_send(chat_id, chunk, reply_to=message_id)
|
||||
self._append_history(history_path, "codex_response", {"jobId": job_id, "text": answer})
|
||||
self._send_private_job_public_report(job, answer)
|
||||
self._send_task_center_reminder(job)
|
||||
if self._voice_replies_enabled(job.get("username") or ""):
|
||||
self._send_voice_reply_for_answer(job, answer, history_path, job_id)
|
||||
self._safe_send(chat_id, f"Готово #{job_num}.", reply_to=message_id)
|
||||
self._mark_job_done(job_id)
|
||||
except Exception as e:
|
||||
if self.stop_current_job:
|
||||
self._append_history(history_path, "job_stopped", {"jobId": job_id, "reason": str(e)})
|
||||
self._safe_send(chat_id, f"Задача #{job_num} остановлена.", reply_to=message_id)
|
||||
self._set_job_status_text(job, f"Задача #{job_num} остановлена.")
|
||||
self._mark_job_removed(job_id)
|
||||
self.stop_current_job = False
|
||||
return
|
||||
@@ -1584,8 +1679,10 @@ class ShinePyBotService:
|
||||
f"Работай в рабочем проекте аккуратно и верни только текст ответа пользователю.{player_block}{retry_block}"
|
||||
)
|
||||
|
||||
def _run_codex(self, prompt: str, chat_id: int, message_id: int, job_id: str, job_num: Any) -> str:
|
||||
def _run_codex(self, prompt: str, job: dict[str, Any]) -> str:
|
||||
output_lines: list[str] = []
|
||||
job_id = str(job["id"])
|
||||
job_num = job.get("num", "?")
|
||||
with tempfile.NamedTemporaryFile(prefix="shine-codex-last-message-", suffix=".txt", delete=False) as tmp:
|
||||
output_file = Path(tmp.name)
|
||||
|
||||
@@ -1624,7 +1721,7 @@ class ShinePyBotService:
|
||||
note = self._extract_codex_user_note(line)
|
||||
now = time.time()
|
||||
if note and note != last_user_note and now - last_user_note_at > 8:
|
||||
self._safe_send(chat_id, f"#{job_num}: {note}", reply_to=message_id)
|
||||
self._set_job_status_text(job, f"Задача #{job_num} в работе.\nСтатус: {note}")
|
||||
last_user_note = note
|
||||
last_user_note_at = now
|
||||
last_job_message_at = now
|
||||
@@ -1656,10 +1753,9 @@ class ShinePyBotService:
|
||||
raise RuntimeError(f"Codex timeout after {self.cfg.codex_timeout_seconds}s")
|
||||
if now - codex_started_at >= 120 and now - last_job_message_at >= 120:
|
||||
elapsed = self._format_duration(int(now - codex_started_at))
|
||||
self._safe_send(
|
||||
chat_id,
|
||||
f"#{job_num}: задача ещё выполняется, работает уже {elapsed}. От Codex давно нет сообщений.",
|
||||
reply_to=message_id,
|
||||
self._set_job_status_text(
|
||||
job,
|
||||
f"Задача #{job_num} в работе.\nСтатус: выполняется уже {elapsed}, от Codex давно нет новых сообщений.",
|
||||
)
|
||||
last_job_message_at = now
|
||||
self.last_heartbeat_at = now
|
||||
@@ -1705,8 +1801,6 @@ class ShinePyBotService:
|
||||
def _handle_job_failure(self, job: dict[str, Any], err: Exception) -> None:
|
||||
job_id = job["id"]
|
||||
job_num = job.get("num", "?")
|
||||
chat_id = int(job["chat_id"])
|
||||
message_id = int(job["message_id"])
|
||||
error_text = str(err).strip() or err.__class__.__name__
|
||||
user_error_text = self._user_error_text(err)
|
||||
retryable = not isinstance(err, VoiceTranscriptionError) or err.retryable
|
||||
@@ -1733,13 +1827,12 @@ class ShinePyBotService:
|
||||
will_retry = False
|
||||
|
||||
if will_retry:
|
||||
self._safe_send(
|
||||
chat_id,
|
||||
self._set_job_status_text(
|
||||
job,
|
||||
f"{user_error_text}\nПовторю задачу #{job_num}: попытка {attempts + 1}/{self.cfg.max_retries}.",
|
||||
reply_to=message_id,
|
||||
)
|
||||
else:
|
||||
self._safe_send(chat_id, f"{user_error_text}\nЗадача #{job_num} остановлена.", reply_to=message_id)
|
||||
self._set_job_status_text(job, f"{user_error_text}\nЗадача #{job_num} остановлена.")
|
||||
|
||||
def _user_error_text(self, err: Exception) -> str:
|
||||
if isinstance(err, VoiceTranscriptionError):
|
||||
@@ -1951,6 +2044,83 @@ class ShinePyBotService:
|
||||
print(f"[py-bot] sendMessage error: {e}", flush=True)
|
||||
return None
|
||||
|
||||
def _safe_edit(self, chat_id: int | str, message_id: int | None, text: str) -> bool:
|
||||
text = (text or "").strip()
|
||||
if not text or not message_id:
|
||||
return False
|
||||
if len(text) > 3900:
|
||||
text = text[:3900] + "\n...[обрезано]"
|
||||
resolved_chat_id: int | str = self._resolve_chat_id(chat_id) if isinstance(chat_id, int) else chat_id
|
||||
try:
|
||||
self.telegram.edit_message_text(resolved_chat_id, message_id, text)
|
||||
return True
|
||||
except Exception as e:
|
||||
error_text = str(e)
|
||||
if "message is not modified" in error_text:
|
||||
return True
|
||||
migrate_to_chat_id = self._extract_migrate_to_chat_id(error_text)
|
||||
if migrate_to_chat_id is not None:
|
||||
if isinstance(resolved_chat_id, int):
|
||||
self._remember_chat_migration(resolved_chat_id, migrate_to_chat_id, "edit_message_error")
|
||||
try:
|
||||
self.telegram.edit_message_text(migrate_to_chat_id, message_id, text)
|
||||
return True
|
||||
except Exception as retry_error:
|
||||
print(f"[py-bot] editMessageText retry after migration error: {retry_error}", flush=True)
|
||||
return False
|
||||
print(f"[py-bot] editMessageText error: {e}", flush=True)
|
||||
return False
|
||||
|
||||
def _ensure_job_status_message(self, job_id: str, chat_id: int, reply_to_message_id: int, text: str) -> int | None:
|
||||
text = (text or "").strip()
|
||||
if not text:
|
||||
return None
|
||||
with self.queue_lock:
|
||||
target = next((j for j in self.queue if j.get("id") == job_id), None)
|
||||
if target:
|
||||
existing_id = target.get("status_message_id")
|
||||
existing_text = (target.get("status_message_text") or "").strip()
|
||||
else:
|
||||
existing_id = None
|
||||
existing_text = ""
|
||||
if existing_id and existing_text == text and self._safe_edit(chat_id, int(existing_id), text):
|
||||
return int(existing_id)
|
||||
if existing_id and self._safe_edit(chat_id, int(existing_id), text):
|
||||
with self.queue_lock:
|
||||
target = next((j for j in self.queue if j.get("id") == job_id), None)
|
||||
if target:
|
||||
target["status_message_text"] = text
|
||||
target["updated_at"] = now_iso()
|
||||
self._persist_queue()
|
||||
return int(existing_id)
|
||||
message_id = self._safe_send(chat_id, text, reply_to=reply_to_message_id)
|
||||
if message_id is not None:
|
||||
with self.queue_lock:
|
||||
target = next((j for j in self.queue if j.get("id") == job_id), None)
|
||||
if target:
|
||||
target["status_message_id"] = message_id
|
||||
target["status_message_text"] = text
|
||||
target["updated_at"] = now_iso()
|
||||
self._persist_queue()
|
||||
return message_id
|
||||
|
||||
def _set_job_status_text(self, job: dict[str, Any], text: str) -> None:
|
||||
if (job.get("chat_type") or "") != "private":
|
||||
self._safe_send(int(job["chat_id"]), text, reply_to=int(job["message_id"]))
|
||||
return
|
||||
if not self._single_status_message_enabled(job.get("username") or ""):
|
||||
self._safe_send(int(job["chat_id"]), text, reply_to=int(job["message_id"]))
|
||||
return
|
||||
message_id = self._ensure_job_status_message(
|
||||
job["id"],
|
||||
int(job["chat_id"]),
|
||||
int(job["message_id"]),
|
||||
text,
|
||||
)
|
||||
if message_id is not None:
|
||||
job["status_message_id"] = message_id
|
||||
job["status_message_text"] = text
|
||||
|
||||
def _request_deferred_restart(self) -> None:
|
||||
if self.restart_requested:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user