SHA256
Убрал long-press меню каналов и обновил deploy-проверку sudo
This commit is contained in:
@@ -28,6 +28,14 @@ DEFAULT_ALLOWED_PLAYERS = ",".join([
|
||||
"dimasol1:Дима",
|
||||
])
|
||||
|
||||
TASK_STATUS_LABELS = {
|
||||
"new": "новая",
|
||||
"approved": "одобрена",
|
||||
"rejected": "отклонена",
|
||||
"needs_work": "на доработку",
|
||||
"done": "сделана",
|
||||
}
|
||||
|
||||
|
||||
def now_iso() -> str:
|
||||
return dt.datetime.now(dt.timezone.utc).isoformat()
|
||||
@@ -57,6 +65,10 @@ def parse_allowed_players(raw: str) -> dict[str, str]:
|
||||
return players
|
||||
|
||||
|
||||
def compact_spaces(text: str) -> str:
|
||||
return re.sub(r"\s+", " ", (text or "").strip())
|
||||
|
||||
|
||||
def split_long_text(text: str, chunk_size: int = 3500) -> list[str]:
|
||||
text = (text or "").strip()
|
||||
if not text:
|
||||
@@ -355,9 +367,12 @@ class ShinePyBotService:
|
||||
self.lock_file = config.data_dir / "py_app.lock"
|
||||
self.history_dir = config.data_dir / "history"
|
||||
self.history_archive_dir = self.history_dir / "archive"
|
||||
self.task_center_dir = config.data_dir / "task_center"
|
||||
self.task_center_file = self.task_center_dir / "items.json"
|
||||
self.max_processed_updates = 5000
|
||||
|
||||
self.queue_lock = threading.RLock()
|
||||
self.task_center_lock = threading.RLock()
|
||||
self.stop_event = threading.Event()
|
||||
self.worker = threading.Thread(target=self._worker_loop, name="shine-py-bot-worker", daemon=True)
|
||||
|
||||
@@ -387,6 +402,54 @@ class ShinePyBotService:
|
||||
uname = normalize_username(username)
|
||||
return self.cfg.allowed_players.get(uname, uname)
|
||||
|
||||
def _display_name(self, username: str) -> str:
|
||||
uname = normalize_username(username)
|
||||
if uname == self.cfg.allowed_username:
|
||||
return "Айдар"
|
||||
return self._player_name(uname)
|
||||
|
||||
def _known_usernames(self) -> dict[str, str]:
|
||||
users = {self.cfg.allowed_username: "Айдар"}
|
||||
users.update(self.cfg.allowed_players)
|
||||
return users
|
||||
|
||||
def _find_user_by_text(self, text: str) -> str:
|
||||
source = normalize_username(text)
|
||||
if source in self._known_usernames():
|
||||
return source
|
||||
source_lower = (text or "").strip().lower()
|
||||
aliases = {
|
||||
"айдар": self.cfg.allowed_username,
|
||||
"айдару": self.cfg.allowed_username,
|
||||
"айдара": self.cfg.allowed_username,
|
||||
"милана": "malvviiina",
|
||||
"милане": "malvviiina",
|
||||
"милану": "malvviiina",
|
||||
"миланы": "malvviiina",
|
||||
"сергей": "zodiaktechnika32",
|
||||
"сергею": "zodiaktechnika32",
|
||||
"сергея": "zodiaktechnika32",
|
||||
"иван": "oidasyda",
|
||||
"ивану": "oidasyda",
|
||||
"ивана": "oidasyda",
|
||||
"ворон": "blackbyrd1",
|
||||
"ворону": "blackbyrd1",
|
||||
"ворона": "blackbyrd1",
|
||||
"дима": "dimasol1",
|
||||
"диме": "dimasol1",
|
||||
"диму": "dimasol1",
|
||||
"димы": "dimasol1",
|
||||
}
|
||||
for alias, username in aliases.items():
|
||||
if re.search(rf"(^|\W){re.escape(alias)}($|\W)", source_lower, flags=re.IGNORECASE):
|
||||
return username
|
||||
for username, name in self._known_usernames().items():
|
||||
if username and username in source_lower:
|
||||
return username
|
||||
if name and name.lower() in source_lower:
|
||||
return username
|
||||
return ""
|
||||
|
||||
def run(self) -> None:
|
||||
self._ensure_dirs()
|
||||
self._acquire_single_instance_lock()
|
||||
@@ -438,6 +501,7 @@ class ShinePyBotService:
|
||||
self.cfg.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.history_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.history_archive_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.task_center_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _acquire_single_instance_lock(self) -> None:
|
||||
self.lock_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -611,7 +675,7 @@ class ShinePyBotService:
|
||||
user_settings = {}
|
||||
settings[uname] = user_settings
|
||||
if not isinstance(user_settings.get("voice_replies_enabled"), bool):
|
||||
user_settings["voice_replies_enabled"] = False
|
||||
user_settings["voice_replies_enabled"] = True
|
||||
if not isinstance(user_settings.get("voice_rewrite_enabled"), bool):
|
||||
user_settings["voice_rewrite_enabled"] = True
|
||||
return user_settings
|
||||
@@ -659,6 +723,155 @@ class ShinePyBotService:
|
||||
history_path = self._current_history_file_for_user(username or self.cfg.allowed_username)
|
||||
self._append_history(history_path, "system_event", {"event": event_type, **payload})
|
||||
|
||||
def _load_task_items(self) -> list[dict[str, Any]]:
|
||||
with self.task_center_lock:
|
||||
if not self.task_center_file.exists():
|
||||
return []
|
||||
try:
|
||||
data = json.loads(self.task_center_file.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return []
|
||||
return data if isinstance(data, list) else []
|
||||
|
||||
def _save_task_items(self, items: list[dict[str, Any]]) -> None:
|
||||
with self.task_center_lock:
|
||||
self.task_center_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = self.task_center_file.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
tmp.replace(self.task_center_file)
|
||||
|
||||
def _next_task_item_id(self, items: list[dict[str, Any]]) -> str:
|
||||
max_num = 0
|
||||
for item in items:
|
||||
raw = str(item.get("id") or "")
|
||||
match = re.match(r"TC-(\d+)$", raw)
|
||||
if match:
|
||||
max_num = max(max_num, int(match.group(1)))
|
||||
return f"TC-{max_num + 1:04d}"
|
||||
|
||||
def _create_task_item(
|
||||
self,
|
||||
*,
|
||||
kind: str,
|
||||
title: str,
|
||||
text: str,
|
||||
source_username: str,
|
||||
target_username: str,
|
||||
source_message_id: int | None = None,
|
||||
source_chat_id: int | None = None,
|
||||
opinion: str = "",
|
||||
) -> dict[str, Any]:
|
||||
items = self._load_task_items()
|
||||
item = {
|
||||
"id": self._next_task_item_id(items),
|
||||
"kind": kind,
|
||||
"status": "new",
|
||||
"title": compact_spaces(title)[:160] or ("Предложение" if kind == "proposal" else "Задача"),
|
||||
"text": (text or "").strip(),
|
||||
"opinion": (opinion or "").strip(),
|
||||
"source_username": normalize_username(source_username),
|
||||
"source_name": self._display_name(source_username),
|
||||
"target_username": normalize_username(target_username),
|
||||
"target_name": self._display_name(target_username),
|
||||
"source_chat_id": source_chat_id,
|
||||
"source_message_id": source_message_id,
|
||||
"created_at": now_iso(),
|
||||
"updated_at": now_iso(),
|
||||
}
|
||||
items.append(item)
|
||||
self._save_task_items(items)
|
||||
self._append_history_event("task_center_item_created", {
|
||||
"itemId": item["id"],
|
||||
"kind": kind,
|
||||
"sourceUsername": item["source_username"],
|
||||
"targetUsername": item["target_username"],
|
||||
"title": item["title"],
|
||||
}, username=source_username)
|
||||
return item
|
||||
|
||||
def _task_items_for_user(self, username: str, *, include_done: bool = False) -> list[dict[str, Any]]:
|
||||
uname = normalize_username(username)
|
||||
items = self._load_task_items()
|
||||
result = [
|
||||
item for item in items
|
||||
if normalize_username(item.get("target_username")) == uname
|
||||
and (include_done or item.get("status") != "done")
|
||||
]
|
||||
return sorted(result, key=lambda x: str(x.get("created_at") or ""))
|
||||
|
||||
def _task_center_counts_text(self, username: str) -> str:
|
||||
counts: dict[str, int] = {}
|
||||
for item in self._task_items_for_user(username):
|
||||
status = str(item.get("status") or "new")
|
||||
counts[status] = counts.get(status, 0) + 1
|
||||
active_total = sum(counts.values())
|
||||
if active_total <= 0:
|
||||
return ""
|
||||
parts = []
|
||||
for status in ("new", "approved", "needs_work", "rejected"):
|
||||
count = counts.get(status, 0)
|
||||
if count:
|
||||
parts.append(f"{TASK_STATUS_LABELS.get(status, status)}: {count}")
|
||||
return f"Напоминание по задачам: всего активных {active_total}; " + ", ".join(parts) + "."
|
||||
|
||||
def _format_task_items(self, username: str, *, include_done: bool = False) -> str:
|
||||
items = self._task_items_for_user(username, include_done=include_done)
|
||||
if not items:
|
||||
return f"Для {self._display_name(username)} активных задач и предложений нет."
|
||||
lines = [f"Задачи и предложения для {self._display_name(username)}:"]
|
||||
for item in items[:15]:
|
||||
kind = "предложение" if item.get("kind") == "proposal" else "задача"
|
||||
status = TASK_STATUS_LABELS.get(str(item.get("status") or "new"), str(item.get("status") or "new"))
|
||||
source = item.get("source_name") or item.get("source_username") or "неизвестно"
|
||||
title = item.get("title") or "(без названия)"
|
||||
lines.append(f"{item.get('id')} [{status}] {kind} от {source}: {title}")
|
||||
if len(items) > 15:
|
||||
lines.append(f"...и ещё {len(items) - 15}")
|
||||
return "\n".join(lines)
|
||||
|
||||
def _update_task_item_status(self, item_id: str, status: str) -> dict[str, Any] | None:
|
||||
item_id = (item_id or "").strip().upper()
|
||||
items = self._load_task_items()
|
||||
updated = None
|
||||
for item in items:
|
||||
if str(item.get("id") or "").upper() == item_id:
|
||||
item["status"] = status
|
||||
item["updated_at"] = now_iso()
|
||||
updated = item
|
||||
break
|
||||
if updated is not None:
|
||||
self._save_task_items(items)
|
||||
return updated
|
||||
|
||||
def _find_first_task_item(self, *, source_username: str = "", target_username: str = "", kind: str = "") -> dict[str, Any] | None:
|
||||
source = normalize_username(source_username)
|
||||
target = normalize_username(target_username)
|
||||
for item in self._load_task_items():
|
||||
if item.get("status") == "done":
|
||||
continue
|
||||
if source and normalize_username(item.get("source_username")) != source:
|
||||
continue
|
||||
if target and normalize_username(item.get("target_username")) != target:
|
||||
continue
|
||||
if kind and item.get("kind") != kind:
|
||||
continue
|
||||
return item
|
||||
return None
|
||||
|
||||
def _notify_user_about_task_item(self, username: str, item: dict[str, Any]) -> None:
|
||||
chat_id = self._private_chat_id_for_user(username)
|
||||
if chat_id is None:
|
||||
return
|
||||
kind = "предложение" if item.get("kind") == "proposal" else "задача"
|
||||
source = item.get("source_name") or item.get("source_username") or "кто-то"
|
||||
title = item.get("title") or "(без названия)"
|
||||
status = TASK_STATUS_LABELS.get(str(item.get("status") or "new"), str(item.get("status") or "new"))
|
||||
if item.get("status") == "new":
|
||||
text = f"У тебя новое {kind} от {source}: {title}\nID: {item.get('id')}"
|
||||
else:
|
||||
text = f"Обновление по {kind} {item.get('id')}: статус «{status}».\n{title}"
|
||||
self._safe_send(chat_id, text)
|
||||
|
||||
def _send_player_welcome_once(self, chat_id: int, message_id: int, username: str) -> None:
|
||||
uname = normalize_username(username)
|
||||
sent = self.state.get("player_welcome_sent")
|
||||
@@ -673,6 +886,9 @@ class ShinePyBotService:
|
||||
"Можно задавать вопросы по проекту, просить анализ, идеи и подготовку готового ТЗ.\n"
|
||||
"Команда /new начинает новую сессию и архивирует текущую историю."
|
||||
)
|
||||
reminder = self._task_center_counts_text(uname)
|
||||
if reminder:
|
||||
text = f"{text}\n\n{reminder}\nКоманда /tasks покажет список."
|
||||
self._safe_send(chat_id, text, reply_to=message_id)
|
||||
sent[uname] = now_iso()
|
||||
self._persist_state()
|
||||
@@ -829,6 +1045,9 @@ class ShinePyBotService:
|
||||
self._handle_command(chat_id, message_id, actor_username, text)
|
||||
return
|
||||
|
||||
if self._handle_task_center_text(chat_id, message_id, actor_username, text):
|
||||
return
|
||||
|
||||
self._append_history(history_path, "incoming_text", {
|
||||
"chatId": chat_id,
|
||||
"messageId": message_id,
|
||||
@@ -933,6 +1152,104 @@ class ShinePyBotService:
|
||||
"active_since": None,
|
||||
}
|
||||
|
||||
def _handle_task_center_text(self, chat_id: int, message_id: int, username: str, text: str) -> bool:
|
||||
source_text = (text or "").strip()
|
||||
lower = source_text.lower()
|
||||
is_owner = self._is_owner(username)
|
||||
|
||||
if re.search(r"\b(покажи|список|какие)\b.*\b(задач|задачи|предложени)", lower):
|
||||
target = username
|
||||
explicit_target = self._find_user_by_text(source_text)
|
||||
if is_owner and explicit_target:
|
||||
target = explicit_target
|
||||
self._safe_send(chat_id, self._format_task_items(target), reply_to=message_id)
|
||||
return True
|
||||
|
||||
if is_owner:
|
||||
assign_match = re.search(
|
||||
r"(?:поставь|добавь|создай|запиши)\s+(?:задачу|задание)\s+(.+?)(?::|\s+-\s+|\s+—\s+)(.+)",
|
||||
source_text,
|
||||
flags=re.IGNORECASE | re.DOTALL,
|
||||
)
|
||||
if assign_match:
|
||||
target = self._find_user_by_text(assign_match.group(1))
|
||||
body = assign_match.group(2).strip()
|
||||
if target and body:
|
||||
item = self._create_task_item(
|
||||
kind="task",
|
||||
title=body,
|
||||
text=body,
|
||||
source_username=username,
|
||||
target_username=target,
|
||||
source_message_id=message_id,
|
||||
source_chat_id=chat_id,
|
||||
)
|
||||
self._notify_user_about_task_item(target, item)
|
||||
self._safe_send(
|
||||
chat_id,
|
||||
f"Задача добавлена для {self._display_name(target)}: {item['id']} — {item['title']}",
|
||||
reply_to=message_id,
|
||||
)
|
||||
return True
|
||||
|
||||
status_match = re.search(r"\b(одобрить|отклонить|доработать|закрыть|сделано|закрыта)\b(?:\s+(.+))?", lower, flags=re.IGNORECASE)
|
||||
if status_match and ("задач" in lower or "предложени" in lower or re.search(r"tc-\d+", lower, flags=re.IGNORECASE)):
|
||||
action = status_match.group(1)
|
||||
tail = status_match.group(2) or ""
|
||||
status = {
|
||||
"одобрить": "approved",
|
||||
"отклонить": "rejected",
|
||||
"доработать": "needs_work",
|
||||
"закрыть": "done",
|
||||
"сделано": "done",
|
||||
"закрыта": "done",
|
||||
}.get(action, "new")
|
||||
id_match = re.search(r"tc-\d+", source_text, flags=re.IGNORECASE)
|
||||
item = None
|
||||
if id_match:
|
||||
item = self._update_task_item_status(id_match.group(0), status)
|
||||
else:
|
||||
source_user = self._find_user_by_text(tail)
|
||||
item = self._find_first_task_item(
|
||||
source_username=source_user,
|
||||
target_username=username,
|
||||
kind="proposal" if "предложени" in lower else "",
|
||||
)
|
||||
if item:
|
||||
item = self._update_task_item_status(str(item.get("id")), status)
|
||||
if item:
|
||||
label = TASK_STATUS_LABELS.get(status, status)
|
||||
self._safe_send(chat_id, f"{item.get('id')} обновлена: {label}.", reply_to=message_id)
|
||||
source_user = normalize_username(item.get("source_username"))
|
||||
if source_user and source_user != username:
|
||||
self._notify_user_about_task_item(source_user, item)
|
||||
return True
|
||||
|
||||
if not is_owner:
|
||||
proposal_match = re.match(r"\s*(?:предложение|идея|заявка)\s*[::-]\s*(.+)", source_text, flags=re.IGNORECASE | re.DOTALL)
|
||||
if proposal_match:
|
||||
body = proposal_match.group(1).strip()
|
||||
if body:
|
||||
item = self._create_task_item(
|
||||
kind="proposal",
|
||||
title=body,
|
||||
text=body,
|
||||
opinion="Нужно решение Айдара: одобрить, отклонить или отправить на доработку.",
|
||||
source_username=username,
|
||||
target_username=self.cfg.allowed_username,
|
||||
source_message_id=message_id,
|
||||
source_chat_id=chat_id,
|
||||
)
|
||||
self._notify_user_about_task_item(self.cfg.allowed_username, item)
|
||||
self._safe_send(
|
||||
chat_id,
|
||||
f"Предложение отправлено Айдару как {item['id']}. Статус: новая.",
|
||||
reply_to=message_id,
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _handle_command(self, chat_id: int, message_id: int, username: str, text: str) -> None:
|
||||
lower = text.lower()
|
||||
command = lower.split(maxsplit=1)[0].split("@", 1)[0]
|
||||
@@ -946,6 +1263,15 @@ class ShinePyBotService:
|
||||
if command == "/queue":
|
||||
self._safe_send(chat_id, self._queue_text(), reply_to=message_id)
|
||||
return
|
||||
if command in ("/tasks", "/my_tasks"):
|
||||
parts = text.split(maxsplit=1)
|
||||
target = username
|
||||
if is_owner and len(parts) > 1:
|
||||
parsed = self._find_user_by_text(parts[1])
|
||||
if parsed:
|
||||
target = parsed
|
||||
self._safe_send(chat_id, self._format_task_items(target), reply_to=message_id)
|
||||
return
|
||||
if command == "/voice_on":
|
||||
self._set_voice_replies_enabled(username, True)
|
||||
self._append_history_event("voice_replies_enabled", {"username": normalize_username(username)}, username=username)
|
||||
@@ -956,14 +1282,8 @@ class ShinePyBotService:
|
||||
self._append_history_event("voice_replies_disabled", {"username": normalize_username(username)}, username=username)
|
||||
self._safe_send(chat_id, "Озвучивание финальных ответов выключено для вашего пользователя.", reply_to=message_id)
|
||||
return
|
||||
if command == "/voice_status":
|
||||
status = "включено" if self._voice_replies_enabled(username) else "выключено"
|
||||
rewrite_status = "включена" if self._voice_rewrite_enabled(username) else "выключена"
|
||||
self._safe_send(
|
||||
chat_id,
|
||||
f"Озвучивание финальных ответов: {status}.\nАдаптация текста перед озвучкой: {rewrite_status}.",
|
||||
reply_to=message_id,
|
||||
)
|
||||
if command in ("/voice_status", "/voice_rewrite_status"):
|
||||
self._safe_send(chat_id, self._status_text(username), reply_to=message_id)
|
||||
return
|
||||
if command == "/voice_rewrite_on":
|
||||
self._set_voice_rewrite_enabled(username, True)
|
||||
@@ -975,10 +1295,6 @@ 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 == "/voice_rewrite_status":
|
||||
status = "включена" if self._voice_rewrite_enabled(username) else "выключена"
|
||||
self._safe_send(chat_id, f"Адаптация текста перед озвучкой: {status}.", 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)
|
||||
@@ -987,17 +1303,33 @@ class ShinePyBotService:
|
||||
if not is_owner:
|
||||
self._safe_send(chat_id, "Команда недоступна.", reply_to=message_id)
|
||||
return
|
||||
self._append_history_event("restart_service_requested", {
|
||||
self._append_history_event("restart_service_deferred_requested", {
|
||||
"chatId": chat_id,
|
||||
"messageId": message_id,
|
||||
"username": username,
|
||||
}, username=username)
|
||||
self._safe_send(
|
||||
chat_id,
|
||||
"Перезапускаю сервис. Если задача была активна, после старта она вернётся в очередь и продолжится.",
|
||||
"Отложенный рестарт принят. Если задача сейчас выполняется, сервис перезапустится после её завершения и до следующей задачи.",
|
||||
reply_to=message_id,
|
||||
)
|
||||
self._schedule_self_restart()
|
||||
self._request_deferred_restart()
|
||||
return
|
||||
if command in ("/restart_hard", "/restart_now", "/restart_force"):
|
||||
if not is_owner:
|
||||
self._safe_send(chat_id, "Команда недоступна.", reply_to=message_id)
|
||||
return
|
||||
self._append_history_event("restart_service_hard_requested", {
|
||||
"chatId": chat_id,
|
||||
"messageId": message_id,
|
||||
"username": username,
|
||||
}, username=username)
|
||||
self._safe_send(
|
||||
chat_id,
|
||||
"Выполняю жёсткий рестарт сервиса прямо сейчас. Активная задача, если есть, будет прервана и после старта вернётся в очередь.",
|
||||
reply_to=message_id,
|
||||
)
|
||||
self._schedule_self_restart("hard_restart_requested", force=True)
|
||||
return
|
||||
if command == "/stop":
|
||||
stopped = self._cancel_active_job("stopped_by_user")
|
||||
@@ -1030,6 +1362,7 @@ class ShinePyBotService:
|
||||
"Доступные команды:",
|
||||
"/status — активная задача и размер очереди",
|
||||
"/queue — список задач в очереди",
|
||||
"/tasks — список ваших задач и предложений",
|
||||
"/stop — остановить текущую задачу",
|
||||
"/cancel <id|all> — удалить задачу по id (префикс) или все",
|
||||
"/new — архивировать историю и начать новую",
|
||||
@@ -1037,11 +1370,12 @@ class ShinePyBotService:
|
||||
"/voice_off — выключить озвучивание финальных ответов",
|
||||
"/voice_rewrite_on — включить адаптацию текста перед озвучкой",
|
||||
"/voice_rewrite_off — выключить адаптацию текста перед озвучкой",
|
||||
"/voice_status — показать состояние голосовых функций",
|
||||
"/help — эта справка",
|
||||
]
|
||||
if is_owner:
|
||||
lines.insert(-1, "/restart_service — перезапустить сервис через systemd")
|
||||
lines.insert(-1, "/tasks <пользователь> — список задач игрока")
|
||||
lines.insert(-1, "/restart — отложенный рестарт после текущей задачи")
|
||||
lines.insert(-1, "/restart_hard — жёсткий рестарт прямо сейчас")
|
||||
return "\n".join(lines)
|
||||
|
||||
def _status_text(self, username: str) -> str:
|
||||
@@ -1054,8 +1388,9 @@ class ShinePyBotService:
|
||||
f"Голосовые ответы: {voice_status}\n"
|
||||
f"Адаптация текста перед озвучкой: {rewrite_status}"
|
||||
)
|
||||
restart_text = "\nОтложенный рестарт: ожидает завершения текущей задачи" if self.restart_requested else ""
|
||||
if not active:
|
||||
return f"Статус: активной задачи нет.\nВ очереди pending: {pending}\n{settings_text}"
|
||||
return f"Статус: активной задачи нет.\nВ очереди pending: {pending}\n{settings_text}{restart_text}"
|
||||
elapsed = int(time.time() - (self.active_job_started_at or time.time()))
|
||||
return (
|
||||
f"Статус: активная задача #{active.get('num', '?')}\n"
|
||||
@@ -1063,7 +1398,7 @@ class ShinePyBotService:
|
||||
f"Попытка: {int(active.get('attempts', 0)) + 1}/{self.cfg.max_retries}\n"
|
||||
f"Выполняется: {elapsed}с\n"
|
||||
f"Pending: {pending}\n"
|
||||
f"{settings_text}"
|
||||
f"{settings_text}{restart_text}"
|
||||
)
|
||||
|
||||
def _queue_text(self) -> str:
|
||||
@@ -1115,6 +1450,9 @@ class ShinePyBotService:
|
||||
|
||||
def _worker_loop(self) -> None:
|
||||
while not self.stop_event.is_set():
|
||||
if self.restart_requested:
|
||||
self._exit_for_restart("deferred_restart_before_next_job")
|
||||
return
|
||||
job = None
|
||||
with self.queue_lock:
|
||||
for item in self.queue:
|
||||
@@ -1135,6 +1473,9 @@ class ShinePyBotService:
|
||||
self._process_job(job)
|
||||
self.active_job_id = None
|
||||
self.active_job_started_at = None
|
||||
if self.restart_requested:
|
||||
self._exit_for_restart("deferred_restart_after_job")
|
||||
return
|
||||
|
||||
def _process_job(self, job: dict[str, Any]) -> None:
|
||||
job_id = job["id"]
|
||||
@@ -1162,6 +1503,7 @@ class ShinePyBotService:
|
||||
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)
|
||||
@@ -1390,6 +1732,15 @@ class ShinePyBotService:
|
||||
self.queue = [j for j in self.queue if j.get("id") != job_id]
|
||||
self._persist_queue()
|
||||
|
||||
def _send_task_center_reminder(self, job: dict[str, Any]) -> None:
|
||||
if job.get("chat_type") != "private":
|
||||
return
|
||||
username = job.get("username") or ""
|
||||
reminder = self._task_center_counts_text(username)
|
||||
if not reminder:
|
||||
return
|
||||
self._safe_send(int(job["chat_id"]), reminder, reply_to=int(job["message_id"]))
|
||||
|
||||
def _remember_public_report_chat(self, chat_id: int) -> None:
|
||||
if self.state.get("public_report_chat_id") == chat_id:
|
||||
return
|
||||
@@ -1575,14 +1926,36 @@ class ShinePyBotService:
|
||||
print(f"[py-bot] sendMessage error: {e}", flush=True)
|
||||
return None
|
||||
|
||||
def _schedule_self_restart(self) -> None:
|
||||
def _request_deferred_restart(self) -> None:
|
||||
if self.restart_requested:
|
||||
return
|
||||
self.restart_requested = True
|
||||
self._append_history_event("restart_service_deferred_scheduled", {})
|
||||
with self.queue_lock:
|
||||
has_active = any(j.get("status") == "active" for j in self.queue)
|
||||
if not has_active:
|
||||
threading.Thread(
|
||||
target=lambda: self._exit_for_restart("deferred_restart_no_active_job"),
|
||||
name="shine-py-bot-deferred-restart",
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
def _exit_for_restart(self, reason: str) -> None:
|
||||
print(f"[py-bot] restart now: {reason}", flush=True)
|
||||
self._append_history_event("restart_service_executing", {"reason": reason})
|
||||
time.sleep(0.5)
|
||||
os._exit(0)
|
||||
|
||||
def _schedule_self_restart(self, reason: str = "restart_requested", *, force: bool = False) -> None:
|
||||
if self.restart_requested and not force:
|
||||
return
|
||||
self.restart_requested = True
|
||||
|
||||
def restart() -> None:
|
||||
time.sleep(1.5)
|
||||
print("[py-bot] restart requested by Telegram command", flush=True)
|
||||
print(f"[py-bot] restart requested by Telegram command: {reason}", flush=True)
|
||||
if force:
|
||||
self._stop_active_codex_process()
|
||||
os._exit(0)
|
||||
|
||||
threading.Thread(target=restart, name="shine-py-bot-self-restart", daemon=True).start()
|
||||
@@ -1725,10 +2098,11 @@ class ShinePyBotService:
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Ты готовишь короткую русскую голосовую версию финального ответа технического агента. "
|
||||
"Сохрани итог, важные предупреждения и действия. Убери длинные пути, хэши, команды, номера версий, "
|
||||
"JSON, списки файлов и другие строки, которые плохо воспринимаются на слух. "
|
||||
"Не добавляй новых фактов. Пиши естественно, кратко, без markdown."
|
||||
"Ты готовишь русскую версию финального ответа технического агента для озвучивания. "
|
||||
"Не пересказывай заново и не меняй смысл: сохрани порядок мыслей, итог, предупреждения, статусы и важные действия. "
|
||||
"Мягко убери только то, что плохо воспринимается на слух: длинные пути, хэши, ID, команды, JSON, "
|
||||
"длинные списки файлов, точные размеры и счётчики символов. Если деталь важна, замени её коротким описанием. "
|
||||
"Не добавляй новых фактов. Пиши естественно, без markdown, близко к исходному тексту."
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user