Compare commits
5 Commits
3a5856c7f0
...
134e877b7c
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
134e877b7c | ||
|
|
b75ac46781 | ||
|
|
edc94d3700 | ||
|
|
b13efa92fd | ||
|
|
6f796c98f7 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
|||||||
data/
|
data/
|
||||||
logs/
|
logs/
|
||||||
logs
|
logs
|
||||||
|
.understand-anything/
|
||||||
|
|
||||||
.gradle
|
.gradle
|
||||||
.gradle-home/
|
.gradle-home/
|
||||||
|
|||||||
1
.idea/vcs.xml
generated
1
.idea/vcs.xml
generated
@ -4,5 +4,6 @@
|
|||||||
<mapping directory="" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
<mapping directory="$PROJECT_DIR$/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo" vcs="Git" />
|
||||||
<mapping directory="$PROJECT_DIR$/shine-solana" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$/shine-solana" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/tools/understand-anything-lab/upstream" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@ -103,8 +103,7 @@
|
|||||||
"serverLastGlobalNumber": 128,
|
"serverLastGlobalNumber": 128,
|
||||||
"serverLastGlobalHash": "4f...ab",
|
"serverLastGlobalHash": "4f...ab",
|
||||||
"serverBlockchainSizeBytes": 45212,
|
"serverBlockchainSizeBytes": 45212,
|
||||||
"serverBlockchainSizeLimitBytes": 100000,
|
"serverBlockchainSizeLimitBytes": 100000
|
||||||
"serverBlocksCount": 129
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -115,7 +114,6 @@
|
|||||||
- `serverLastGlobalHash` — hash последнего блока (hex-строка 64 символа);
|
- `serverLastGlobalHash` — hash последнего блока (hex-строка 64 символа);
|
||||||
- `serverBlockchainSizeBytes` — текущий размер пользовательского блокчейна на сервере в байтах;
|
- `serverBlockchainSizeBytes` — текущий размер пользовательского блокчейна на сервере в байтах;
|
||||||
- `serverBlockchainSizeLimitBytes` — текущий лимит размера блокчейна на сервере в байтах;
|
- `serverBlockchainSizeLimitBytes` — текущий лимит размера блокчейна на сервере в байтах;
|
||||||
- `serverBlocksCount` — количество блоков в пользовательском блокчейне на сервере;
|
|
||||||
|
|
||||||
### Успешный ответ: пользователя нет
|
### Успешный ответ: пользователя нет
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
# Голосовая адаптация ответов Telegram-бота
|
||||||
|
|
||||||
|
## Краткое описание
|
||||||
|
Добавлены персональные настройки голосовых ответов и адаптации текста перед озвучкой. Если голосовые ответы включены, сервис перед TTS может отдельно прогонять финальный текст через OpenAI-модель и отправлять более короткую голосовую версию в исходный чат, личный чат пользователя и общий чат `@shine_writing`, если эти чаты доступны и отличаются.
|
||||||
|
|
||||||
|
## Что проверить
|
||||||
|
- Команды `/voice_on`, `/voice_off`, `/voice_status` для конкретного пользователя.
|
||||||
|
- Команды `/voice_rewrite_on`, `/voice_rewrite_off`, `/voice_rewrite_status` для конкретного пользователя.
|
||||||
|
- Команда `/status` показывает очередь, голосовые ответы и адаптацию текста перед озвучкой.
|
||||||
|
- При включённых голосовых ответах после задачи приходит текстовый ответ и voice-ответ.
|
||||||
|
- При включённой адаптации voice-ответ короче и без длинных технических строк.
|
||||||
|
- При задаче из личного чата voice дополнительно появляется в общем чате `@shine_writing`.
|
||||||
|
- При задаче из общего чата voice дополнительно появляется в личном чате пользователя, если сервис уже знает его личный chat_id.
|
||||||
|
|
||||||
|
## Ожидаемый результат
|
||||||
|
Текстовый ответ остаётся полным. Голосовая версия приходит отдельно, звучит короче и естественнее, а персональные настройки одного пользователя не меняют поведение других пользователей.
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
pending
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
# Эксперимент Understand Anything
|
||||||
|
|
||||||
|
## Краткое описание
|
||||||
|
|
||||||
|
Добавлена изолированная лаборатория для проверки `Lum1104/Understand-Anything` без подключения к сборке, деплою и рабочему коду SHiNE.
|
||||||
|
|
||||||
|
## Что проверять
|
||||||
|
|
||||||
|
- Установить Node.js 22+ и pnpm 10+.
|
||||||
|
- Запустить `./tools/understand-anything-lab/install_codex_skills.sh`.
|
||||||
|
- Перезапустить Codex-сессию.
|
||||||
|
- Выполнить `/understand --language ru` в корне проекта.
|
||||||
|
- После генерации выполнить `/understand-dashboard` и проверить, что граф открывается и помогает ориентироваться по серверным, UI, Solana и агентским папкам.
|
||||||
|
|
||||||
|
## Ожидаемый результат
|
||||||
|
|
||||||
|
- В проекте появляется локальная папка `.understand-anything/` с графом знаний.
|
||||||
|
- Dashboard открывается и показывает интерактивный граф проекта.
|
||||||
|
- Основные процессы сборки и деплоя SHiNE не меняются.
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
|
||||||
|
`pending`
|
||||||
|
|
||||||
@ -33,7 +33,16 @@
|
|||||||
- После успешной обработки задачи из личного чата Айдара сервис должен отправить публичный итоговый отчёт в группу `@shine_writing`: первым сообщением исходный запрос, вторым сообщением-ответом итоговый ответ Codex. Промежуточные статусы в группу не дублировать.
|
- После успешной обработки задачи из личного чата Айдара сервис должен отправить публичный итоговый отчёт в группу `@shine_writing`: первым сообщением исходный запрос, вторым сообщением-ответом итоговый ответ Codex. Промежуточные статусы в группу не дублировать.
|
||||||
- Для приватных voice/audio-запросов в публичном отчёте первым сообщением отправлять исходный Telegram voice/audio-файл с подписью, где указан распознанный текст. В пользовательском тексте отчёта не показывать Telegram `file_id`.
|
- Для приватных voice/audio-запросов в публичном отчёте первым сообщением отправлять исходный Telegram voice/audio-файл с подписью, где указан распознанный текст. В пользовательском тексте отчёта не показывать Telegram `file_id`.
|
||||||
- Озвучивание финальных ответов настраивается персонально для каждого Telegram-пользователя командами `/voice_on`, `/voice_off`, `/voice_status`.
|
- Озвучивание финальных ответов настраивается персонально для каждого Telegram-пользователя командами `/voice_on`, `/voice_off`, `/voice_status`.
|
||||||
- Если озвучивание включено, после полного текстового финального ответа сервис дополнительно отправляет voice-файл с синтезированной речью через OpenAI TTS. Промежуточные статусы и публичный отчёт в `@shine_writing` не озвучивать.
|
- Адаптация текста перед озвучкой настраивается персонально командами `/voice_rewrite_on`, `/voice_rewrite_off`, `/voice_rewrite_status`. Если она включена, сервис перед TTS вызывает дешёвую текстовую модель OpenAI и делает короткую голосовую версию без длинных хэшей, путей, команд и технического шума.
|
||||||
|
- Если озвучивание включено, после полного текстового финального ответа сервис дополнительно отправляет voice-файл с синтезированной речью через OpenAI TTS. Voice отправляется в исходный чат, а также в известный личный чат пользователя и в общий чат `@shine_writing`, если они отличаются и доступны. Промежуточные статусы не озвучивать.
|
||||||
|
- Команда `/status` должна показывать состояние очереди и персональные состояния голосовых функций: включены ли voice-ответы и адаптация текста перед озвучкой.
|
||||||
|
|
||||||
|
## Правила голосовой версии ответа
|
||||||
|
- Текстовый финальный ответ должен оставаться полноценным: в нём можно указывать команды, пути, хэши коммитов, номера версий, результаты проверок и другие технические детали.
|
||||||
|
- Голосовую версию финального ответа нужно делать короче и проще для восприятия на слух. Основной механизм — персонально включаемая адаптация текста через дополнительный OpenAI-вызов перед TTS.
|
||||||
|
- В голосовой версии не зачитывать длинные хэши коммитов, токены, file_id, длинные команды, полные пути и другие строки, которые человек всё равно не сможет надёжно запомнить на слух.
|
||||||
|
- Для commit/push в голосовой версии достаточно сказать краткий итог: что коммит сделан, что именно изменено, проверки прошли без ошибок, push выполнен, рабочее дерево чистое.
|
||||||
|
- Если пользователю нужны точные команды, хэши или подробности, они должны оставаться в текстовом ответе.
|
||||||
|
|
||||||
## Планы и отложенные фичи
|
## Планы и отложенные фичи
|
||||||
- Планы проекта по отложенным фичам хранятся в `Dev_Docs/Future_Features/`.
|
- Планы проекта по отложенным фичам хранятся в `Dev_Docs/Future_Features/`.
|
||||||
|
|||||||
@ -322,6 +322,10 @@ class BotConfig:
|
|||||||
self.openai_tts_response_format = env.get("OPENAI_TTS_RESPONSE_FORMAT", "opus")
|
self.openai_tts_response_format = env.get("OPENAI_TTS_RESPONSE_FORMAT", "opus")
|
||||||
self.openai_tts_timeout_seconds = int(env.get("OPENAI_TTS_TIMEOUT_SECONDS", "180"))
|
self.openai_tts_timeout_seconds = int(env.get("OPENAI_TTS_TIMEOUT_SECONDS", "180"))
|
||||||
self.openai_tts_chunk_chars = max(500, int(env.get("OPENAI_TTS_CHUNK_CHARS", "3500")))
|
self.openai_tts_chunk_chars = max(500, int(env.get("OPENAI_TTS_CHUNK_CHARS", "3500")))
|
||||||
|
self.openai_voice_rewrite_model = env.get("OPENAI_VOICE_REWRITE_MODEL", "gpt-4.1-nano")
|
||||||
|
self.openai_voice_rewrite_timeout_seconds = int(env.get("OPENAI_VOICE_REWRITE_TIMEOUT_SECONDS", "90"))
|
||||||
|
self.openai_voice_rewrite_max_input_chars = max(1000, int(env.get("OPENAI_VOICE_REWRITE_MAX_INPUT_CHARS", "12000")))
|
||||||
|
self.openai_voice_rewrite_max_output_tokens = max(200, int(env.get("OPENAI_VOICE_REWRITE_MAX_OUTPUT_TOKENS", "900")))
|
||||||
self.codex_bin = Path(env.get(
|
self.codex_bin = Path(env.get(
|
||||||
"CODEX_BIN",
|
"CODEX_BIN",
|
||||||
"/home/ai/.cache/JetBrains/IntelliJIdea2026.1/aia/codex/bin/codex-x86_64-unknown-linux-musl"
|
"/home/ai/.cache/JetBrains/IntelliJIdea2026.1/aia/codex/bin/codex-x86_64-unknown-linux-musl"
|
||||||
@ -608,6 +612,8 @@ class ShinePyBotService:
|
|||||||
settings[uname] = user_settings
|
settings[uname] = user_settings
|
||||||
if not isinstance(user_settings.get("voice_replies_enabled"), bool):
|
if not isinstance(user_settings.get("voice_replies_enabled"), bool):
|
||||||
user_settings["voice_replies_enabled"] = False
|
user_settings["voice_replies_enabled"] = False
|
||||||
|
if not isinstance(user_settings.get("voice_rewrite_enabled"), bool):
|
||||||
|
user_settings["voice_rewrite_enabled"] = True
|
||||||
return user_settings
|
return user_settings
|
||||||
|
|
||||||
def _voice_replies_enabled(self, username: str) -> bool:
|
def _voice_replies_enabled(self, username: str) -> bool:
|
||||||
@ -617,6 +623,33 @@ class ShinePyBotService:
|
|||||||
self._user_settings(username)["voice_replies_enabled"] = enabled
|
self._user_settings(username)["voice_replies_enabled"] = enabled
|
||||||
self._persist_state()
|
self._persist_state()
|
||||||
|
|
||||||
|
def _voice_rewrite_enabled(self, username: str) -> bool:
|
||||||
|
return bool(self._user_settings(username).get("voice_rewrite_enabled"))
|
||||||
|
|
||||||
|
def _set_voice_rewrite_enabled(self, username: str, enabled: bool) -> None:
|
||||||
|
self._user_settings(username)["voice_rewrite_enabled"] = enabled
|
||||||
|
self._persist_state()
|
||||||
|
|
||||||
|
def _remember_private_chat(self, username: str, chat_id: int) -> None:
|
||||||
|
uname = normalize_username(username)
|
||||||
|
if not uname:
|
||||||
|
return
|
||||||
|
private_chats = self.state.get("private_chat_ids")
|
||||||
|
if not isinstance(private_chats, dict):
|
||||||
|
private_chats = {}
|
||||||
|
self.state["private_chat_ids"] = private_chats
|
||||||
|
if private_chats.get(uname) == chat_id:
|
||||||
|
return
|
||||||
|
private_chats[uname] = chat_id
|
||||||
|
self._persist_state()
|
||||||
|
|
||||||
|
def _private_chat_id_for_user(self, username: str) -> int | None:
|
||||||
|
private_chats = self.state.get("private_chat_ids")
|
||||||
|
if not isinstance(private_chats, dict):
|
||||||
|
return None
|
||||||
|
chat_id = private_chats.get(normalize_username(username))
|
||||||
|
return self._resolve_chat_id(chat_id) if isinstance(chat_id, int) else None
|
||||||
|
|
||||||
def _append_history(self, history_path: Path, event_type: str, payload: dict[str, Any]) -> None:
|
def _append_history(self, history_path: Path, event_type: str, payload: dict[str, Any]) -> None:
|
||||||
row = {"ts": now_iso(), "type": event_type}
|
row = {"ts": now_iso(), "type": event_type}
|
||||||
row.update(payload)
|
row.update(payload)
|
||||||
@ -754,6 +787,8 @@ class ShinePyBotService:
|
|||||||
return
|
return
|
||||||
|
|
||||||
self._ensure_user_session(actor_username)
|
self._ensure_user_session(actor_username)
|
||||||
|
if is_private:
|
||||||
|
self._remember_private_chat(actor_username, chat_id)
|
||||||
history_path = self._current_history_file_for_user(actor_username)
|
history_path = self._current_history_file_for_user(actor_username)
|
||||||
if self._is_allowed_player(actor_username):
|
if self._is_allowed_player(actor_username):
|
||||||
self._send_player_welcome_once(chat_id, message_id, actor_username)
|
self._send_player_welcome_once(chat_id, message_id, actor_username)
|
||||||
@ -906,7 +941,7 @@ class ShinePyBotService:
|
|||||||
self._safe_send(chat_id, self._help_text(is_owner=is_owner), reply_to=message_id)
|
self._safe_send(chat_id, self._help_text(is_owner=is_owner), reply_to=message_id)
|
||||||
return
|
return
|
||||||
if command == "/status":
|
if command == "/status":
|
||||||
self._safe_send(chat_id, self._status_text(), reply_to=message_id)
|
self._safe_send(chat_id, self._status_text(username), reply_to=message_id)
|
||||||
return
|
return
|
||||||
if command == "/queue":
|
if command == "/queue":
|
||||||
self._safe_send(chat_id, self._queue_text(), reply_to=message_id)
|
self._safe_send(chat_id, self._queue_text(), reply_to=message_id)
|
||||||
@ -923,7 +958,26 @@ class ShinePyBotService:
|
|||||||
return
|
return
|
||||||
if command == "/voice_status":
|
if command == "/voice_status":
|
||||||
status = "включено" if self._voice_replies_enabled(username) else "выключено"
|
status = "включено" if self._voice_replies_enabled(username) else "выключено"
|
||||||
self._safe_send(chat_id, f"Озвучивание финальных ответов: {status}.", reply_to=message_id)
|
rewrite_status = "включена" if self._voice_rewrite_enabled(username) else "выключена"
|
||||||
|
self._safe_send(
|
||||||
|
chat_id,
|
||||||
|
f"Озвучивание финальных ответов: {status}.\nАдаптация текста перед озвучкой: {rewrite_status}.",
|
||||||
|
reply_to=message_id,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if command == "/voice_rewrite_on":
|
||||||
|
self._set_voice_rewrite_enabled(username, True)
|
||||||
|
self._append_history_event("voice_rewrite_enabled", {"username": normalize_username(username)}, username=username)
|
||||||
|
self._safe_send(chat_id, "Адаптация текста перед озвучкой включена для вашего пользователя.", reply_to=message_id)
|
||||||
|
return
|
||||||
|
if command == "/voice_rewrite_off":
|
||||||
|
self._set_voice_rewrite_enabled(username, False)
|
||||||
|
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
|
return
|
||||||
if command == "/new":
|
if command == "/new":
|
||||||
archived = self._rotate_history("command_new", username)
|
archived = self._rotate_history("command_new", username)
|
||||||
@ -981,26 +1035,35 @@ class ShinePyBotService:
|
|||||||
"/new — архивировать историю и начать новую",
|
"/new — архивировать историю и начать новую",
|
||||||
"/voice_on — включить озвучивание финальных ответов",
|
"/voice_on — включить озвучивание финальных ответов",
|
||||||
"/voice_off — выключить озвучивание финальных ответов",
|
"/voice_off — выключить озвучивание финальных ответов",
|
||||||
"/voice_status — показать состояние озвучивания",
|
"/voice_rewrite_on — включить адаптацию текста перед озвучкой",
|
||||||
|
"/voice_rewrite_off — выключить адаптацию текста перед озвучкой",
|
||||||
|
"/voice_status — показать состояние голосовых функций",
|
||||||
"/help — эта справка",
|
"/help — эта справка",
|
||||||
]
|
]
|
||||||
if is_owner:
|
if is_owner:
|
||||||
lines.insert(-1, "/restart_service — перезапустить сервис через systemd")
|
lines.insert(-1, "/restart_service — перезапустить сервис через systemd")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def _status_text(self) -> str:
|
def _status_text(self, username: str) -> str:
|
||||||
with self.queue_lock:
|
with self.queue_lock:
|
||||||
active = next((j for j in self.queue if j.get("status") == "active"), None)
|
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")
|
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 "выключена"
|
||||||
|
settings_text = (
|
||||||
|
f"Голосовые ответы: {voice_status}\n"
|
||||||
|
f"Адаптация текста перед озвучкой: {rewrite_status}"
|
||||||
|
)
|
||||||
if not active:
|
if not active:
|
||||||
return f"Статус: активной задачи нет.\nВ очереди pending: {pending}"
|
return f"Статус: активной задачи нет.\nВ очереди pending: {pending}\n{settings_text}"
|
||||||
elapsed = int(time.time() - (self.active_job_started_at or time.time()))
|
elapsed = int(time.time() - (self.active_job_started_at or time.time()))
|
||||||
return (
|
return (
|
||||||
f"Статус: активная задача #{active.get('num', '?')}\n"
|
f"Статус: активная задача #{active.get('num', '?')}\n"
|
||||||
f"Тип: {active.get('type', 'text')}\n"
|
f"Тип: {active.get('type', 'text')}\n"
|
||||||
f"Попытка: {int(active.get('attempts', 0)) + 1}/{self.cfg.max_retries}\n"
|
f"Попытка: {int(active.get('attempts', 0)) + 1}/{self.cfg.max_retries}\n"
|
||||||
f"Выполняется: {elapsed}с\n"
|
f"Выполняется: {elapsed}с\n"
|
||||||
f"Pending: {pending}"
|
f"Pending: {pending}\n"
|
||||||
|
f"{settings_text}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _queue_text(self) -> str:
|
def _queue_text(self) -> str:
|
||||||
@ -1097,12 +1160,12 @@ class ShinePyBotService:
|
|||||||
answer = self._run_codex(prompt, chat_id, message_id, job_id, job_num)
|
answer = self._run_codex(prompt, chat_id, message_id, job_id, job_num)
|
||||||
for chunk in split_long_text(answer):
|
for chunk in split_long_text(answer):
|
||||||
self._safe_send(chat_id, chunk, reply_to=message_id)
|
self._safe_send(chat_id, chunk, reply_to=message_id)
|
||||||
if self._voice_replies_enabled(job.get("username") or ""):
|
|
||||||
self._send_voice_reply_for_answer(chat_id, message_id, job_num, answer, history_path, job_id)
|
|
||||||
self._safe_send(chat_id, f"Готово #{job_num}.", reply_to=message_id)
|
|
||||||
self._append_history(history_path, "codex_response", {"jobId": job_id, "text": answer})
|
self._append_history(history_path, "codex_response", {"jobId": job_id, "text": answer})
|
||||||
self._mark_job_done(job_id)
|
|
||||||
self._send_private_job_public_report(job, answer)
|
self._send_private_job_public_report(job, answer)
|
||||||
|
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:
|
except Exception as e:
|
||||||
if self.stop_current_job:
|
if self.stop_current_job:
|
||||||
self._append_history(history_path, "job_stopped", {"jobId": job_id, "reason": str(e)})
|
self._append_history(history_path, "job_stopped", {"jobId": job_id, "reason": str(e)})
|
||||||
@ -1524,28 +1587,85 @@ class ShinePyBotService:
|
|||||||
|
|
||||||
threading.Thread(target=restart, name="shine-py-bot-self-restart", daemon=True).start()
|
threading.Thread(target=restart, name="shine-py-bot-self-restart", daemon=True).start()
|
||||||
|
|
||||||
|
def _voice_reply_targets(self, job: dict[str, Any]) -> list[tuple[int | str, int | None, str]]:
|
||||||
|
chat_id = int(job["chat_id"])
|
||||||
|
message_id = int(job["message_id"])
|
||||||
|
targets: list[tuple[int | str, int | None, str]] = [(chat_id, message_id, "source")]
|
||||||
|
username = job.get("username") or ""
|
||||||
|
|
||||||
|
private_chat_id = self._private_chat_id_for_user(username)
|
||||||
|
if private_chat_id is not None and private_chat_id != self._resolve_chat_id(chat_id):
|
||||||
|
targets.append((private_chat_id, None, "private"))
|
||||||
|
|
||||||
|
report_chat_id = self._public_report_chat_id()
|
||||||
|
if report_chat_id is not None:
|
||||||
|
resolved_report_chat_id = self._resolve_chat_id(report_chat_id) if isinstance(report_chat_id, int) else report_chat_id
|
||||||
|
resolved_current_chat_id: int | str = self._resolve_chat_id(chat_id)
|
||||||
|
if resolved_report_chat_id != resolved_current_chat_id:
|
||||||
|
targets.append((report_chat_id, None, "public"))
|
||||||
|
|
||||||
|
deduped: list[tuple[int | str, int | None, str]] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for target_chat_id, target_reply_to, label in targets:
|
||||||
|
resolved: int | str = self._resolve_chat_id(target_chat_id) if isinstance(target_chat_id, int) else target_chat_id
|
||||||
|
key = str(resolved)
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
deduped.append((target_chat_id, target_reply_to, label))
|
||||||
|
return deduped
|
||||||
|
|
||||||
def _send_voice_reply_for_answer(
|
def _send_voice_reply_for_answer(
|
||||||
self,
|
self,
|
||||||
chat_id: int,
|
job: dict[str, Any],
|
||||||
message_id: int,
|
|
||||||
job_num: Any,
|
|
||||||
answer: str,
|
answer: str,
|
||||||
history_path: Path,
|
history_path: Path,
|
||||||
job_id: str,
|
job_id: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
chat_id = int(job["chat_id"])
|
||||||
|
message_id = int(job["message_id"])
|
||||||
|
job_num = job.get("num", "?")
|
||||||
|
username = job.get("username") or ""
|
||||||
if not self.cfg.openai_api_key:
|
if not self.cfg.openai_api_key:
|
||||||
note = "не настроен ключ OpenAI для озвучивания."
|
note = "не настроен ключ OpenAI для озвучивания."
|
||||||
self._append_history(history_path, "voice_reply_failed", {"jobId": job_id, "jobNum": job_num, "error": note})
|
self._append_history(history_path, "voice_reply_failed", {"jobId": job_id, "jobNum": job_num, "error": note})
|
||||||
self._safe_send(chat_id, f"Озвучивание включено, но {note}", reply_to=message_id)
|
self._safe_send(chat_id, f"Озвучивание включено, но {note}", reply_to=message_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
chunks = split_text_for_tts(answer, self.cfg.openai_tts_chunk_chars)
|
voice_text = answer
|
||||||
|
rewrite_enabled = self._voice_rewrite_enabled(username)
|
||||||
|
if rewrite_enabled:
|
||||||
|
try:
|
||||||
|
voice_text = self._openai_rewrite_text_for_voice(answer)
|
||||||
|
self._append_history(history_path, "voice_rewrite_done", {
|
||||||
|
"jobId": job_id,
|
||||||
|
"jobNum": job_num,
|
||||||
|
"model": self.cfg.openai_voice_rewrite_model,
|
||||||
|
"sourceChars": len(answer or ""),
|
||||||
|
"resultChars": len(voice_text or ""),
|
||||||
|
})
|
||||||
|
except VoiceReplyError as e:
|
||||||
|
self._append_history(history_path, "voice_rewrite_failed", {
|
||||||
|
"jobId": job_id,
|
||||||
|
"jobNum": job_num,
|
||||||
|
"model": self.cfg.openai_voice_rewrite_model,
|
||||||
|
"error": str(e),
|
||||||
|
})
|
||||||
|
self._safe_send(
|
||||||
|
chat_id,
|
||||||
|
f"Не удалось адаптировать ответ #{job_num} для озвучки, озвучиваю обычный текст: {e}",
|
||||||
|
reply_to=message_id,
|
||||||
|
)
|
||||||
|
voice_text = answer
|
||||||
|
|
||||||
|
chunks = split_text_for_tts(voice_text, self.cfg.openai_tts_chunk_chars)
|
||||||
if not chunks:
|
if not chunks:
|
||||||
return
|
return
|
||||||
|
|
||||||
sent_count = 0
|
sent_count = 0
|
||||||
total = len(chunks)
|
total = len(chunks)
|
||||||
print(f"[py-bot] tts start job={str(job_id)[:8]} chunks={total}", flush=True)
|
targets = self._voice_reply_targets(job)
|
||||||
|
print(f"[py-bot] tts start job={str(job_id)[:8]} chunks={total} targets={len(targets)} rewrite={rewrite_enabled}", flush=True)
|
||||||
for index, chunk in enumerate(chunks, start=1):
|
for index, chunk in enumerate(chunks, start=1):
|
||||||
try:
|
try:
|
||||||
audio = self._openai_tts(chunk)
|
audio = self._openai_tts(chunk)
|
||||||
@ -1562,27 +1682,99 @@ class ShinePyBotService:
|
|||||||
caption = f"Озвучка ответа #{job_num}"
|
caption = f"Озвучка ответа #{job_num}"
|
||||||
if total > 1:
|
if total > 1:
|
||||||
caption += f", часть {index}/{total}"
|
caption += f", часть {index}/{total}"
|
||||||
message_sent = self._safe_send_voice_upload(
|
for target_chat_id, target_reply_to, target_label in targets:
|
||||||
chat_id,
|
message_sent = self._safe_send_voice_upload(
|
||||||
audio,
|
target_chat_id,
|
||||||
f"shine-answer-{job_num}-{index}.ogg",
|
audio,
|
||||||
caption=caption,
|
f"shine-answer-{job_num}-{index}.ogg",
|
||||||
reply_to=message_id,
|
caption=caption,
|
||||||
)
|
reply_to=target_reply_to,
|
||||||
if message_sent is None:
|
)
|
||||||
self._append_history(history_path, "voice_reply_failed", {
|
if message_sent is None:
|
||||||
"jobId": job_id,
|
self._append_history(history_path, "voice_reply_failed", {
|
||||||
"jobNum": job_num,
|
"jobId": job_id,
|
||||||
"part": index,
|
"jobNum": job_num,
|
||||||
"parts": total,
|
"part": index,
|
||||||
"error": "Telegram не принял voice-файл озвучки.",
|
"parts": total,
|
||||||
})
|
"target": target_label,
|
||||||
self._safe_send(chat_id, f"Озвучка ответа #{job_num} создана, но Telegram не принял voice-файл.", reply_to=message_id)
|
"error": "Telegram не принял voice-файл озвучки.",
|
||||||
return
|
})
|
||||||
sent_count += 1
|
if target_label == "source":
|
||||||
self._append_history(history_path, "voice_reply_sent", {"jobId": job_id, "jobNum": job_num, "parts": sent_count})
|
self._safe_send(chat_id, f"Озвучка ответа #{job_num} создана, но Telegram не принял voice-файл.", reply_to=message_id)
|
||||||
|
continue
|
||||||
|
sent_count += 1
|
||||||
|
self._append_history(history_path, "voice_reply_sent", {
|
||||||
|
"jobId": job_id,
|
||||||
|
"jobNum": job_num,
|
||||||
|
"parts": total,
|
||||||
|
"messages": sent_count,
|
||||||
|
"targets": len(targets),
|
||||||
|
"rewriteEnabled": rewrite_enabled,
|
||||||
|
})
|
||||||
print(f"[py-bot] tts done job={str(job_id)[:8]} sent={sent_count}", flush=True)
|
print(f"[py-bot] tts done job={str(job_id)[:8]} sent={sent_count}", flush=True)
|
||||||
|
|
||||||
|
def _openai_rewrite_text_for_voice(self, text: str) -> str:
|
||||||
|
source = (text or "").strip()
|
||||||
|
if not source:
|
||||||
|
return ""
|
||||||
|
if len(source) > self.cfg.openai_voice_rewrite_max_input_chars:
|
||||||
|
source = source[:self.cfg.openai_voice_rewrite_max_input_chars].rstrip() + "\n\n...[текстовый ответ был длиннее и обрезан для голосовой версии]"
|
||||||
|
payload = {
|
||||||
|
"model": self.cfg.openai_voice_rewrite_model,
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": (
|
||||||
|
"Ты готовишь короткую русскую голосовую версию финального ответа технического агента. "
|
||||||
|
"Сохрани итог, важные предупреждения и действия. Убери длинные пути, хэши, команды, номера версий, "
|
||||||
|
"JSON, списки файлов и другие строки, которые плохо воспринимаются на слух. "
|
||||||
|
"Не добавляй новых фактов. Пиши естественно, кратко, без markdown."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": f"Переделай этот финальный текст в вариант для озвучки:\n\n{source}",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"temperature": 0.2,
|
||||||
|
"max_tokens": self.cfg.openai_voice_rewrite_max_output_tokens,
|
||||||
|
}
|
||||||
|
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||||
|
req = request.Request("https://api.openai.com/v1/chat/completions", method="POST", data=data)
|
||||||
|
req.add_header("Authorization", f"Bearer {self.cfg.openai_api_key}")
|
||||||
|
req.add_header("Content-Type", "application/json")
|
||||||
|
try:
|
||||||
|
with request.urlopen(req, timeout=self.cfg.openai_voice_rewrite_timeout_seconds) as resp:
|
||||||
|
raw = resp.read().decode("utf-8", errors="replace")
|
||||||
|
except TimeoutError as e:
|
||||||
|
raise VoiceReplyError(
|
||||||
|
f"OpenAI не успел адаптировать текст за {self.cfg.openai_voice_rewrite_timeout_seconds} секунд."
|
||||||
|
) from e
|
||||||
|
except error.HTTPError as e:
|
||||||
|
detail = e.read().decode("utf-8", errors="replace")
|
||||||
|
if e.code == 401:
|
||||||
|
message = "OpenAI отклонил ключ API для адаптации текста."
|
||||||
|
elif e.code == 429:
|
||||||
|
message = "OpenAI временно ограничил адаптацию текста из-за лимита запросов."
|
||||||
|
elif e.code >= 500:
|
||||||
|
message = "OpenAI временно не смог адаптировать текст."
|
||||||
|
else:
|
||||||
|
message = f"OpenAI вернул ошибку HTTP {e.code} при адаптации текста."
|
||||||
|
if detail:
|
||||||
|
message = f"{message} Детали: {detail[:500]}"
|
||||||
|
raise VoiceReplyError(message) from e
|
||||||
|
except error.URLError as e:
|
||||||
|
raise VoiceReplyError(f"не удалось отправить текст в OpenAI для адаптации из-за сетевой ошибки: {e.reason}") from e
|
||||||
|
|
||||||
|
try:
|
||||||
|
body = json.loads(raw)
|
||||||
|
content = (((body.get("choices") or [{}])[0].get("message") or {}).get("content") or "").strip()
|
||||||
|
except Exception as e:
|
||||||
|
raise VoiceReplyError("OpenAI вернул неразборчивый ответ при адаптации текста.") from e
|
||||||
|
if not content:
|
||||||
|
raise VoiceReplyError("OpenAI вернул пустой текст адаптации.")
|
||||||
|
return content
|
||||||
|
|
||||||
def _openai_tts(self, text: str) -> bytes:
|
def _openai_tts(self, text: str) -> bytes:
|
||||||
payload = {
|
payload = {
|
||||||
"model": self.cfg.openai_tts_model,
|
"model": self.cfg.openai_tts_model,
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.96
|
client.version=1.2.101
|
||||||
server.version=1.2.90
|
server.version=1.2.95
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
<link rel="manifest" href="./manifest.webmanifest" />
|
<link rel="manifest" href="./manifest.webmanifest" />
|
||||||
<title>Shine UI Demo</title>
|
<title>Shine UI Demo</title>
|
||||||
<script>
|
<script>
|
||||||
window.__SHINE_BUILD_HASH__ = '20260528010500';
|
window.__SHINE_BUILD_HASH__ = '20260530000700';
|
||||||
window.__SHINE_CLIENT_VERSION__ = '1.2.8';
|
window.__SHINE_CLIENT_VERSION__ = '1.2.8';
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@ -47,7 +47,7 @@ import * as keyStorageView from './pages/key-storage-view.js';
|
|||||||
|
|
||||||
import * as profileView from './pages/profile-view.js';
|
import * as profileView from './pages/profile-view.js';
|
||||||
import * as profileEditView from './pages/profile-edit-view.js';
|
import * as profileEditView from './pages/profile-edit-view.js';
|
||||||
import * as walletView from './pages/wallet-view.js?v=2026052806';
|
import * as walletView from './pages/wallet-view.js?v=202605300007';
|
||||||
import * as settingsView from './pages/settings-view.js';
|
import * as settingsView from './pages/settings-view.js';
|
||||||
import * as developerSettingsView from './pages/developer-settings-view.js';
|
import * as developerSettingsView from './pages/developer-settings-view.js';
|
||||||
import * as serverSettingsView from './pages/server-settings-view.js';
|
import * as serverSettingsView from './pages/server-settings-view.js';
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import {
|
|||||||
getShineBlockchainUsage,
|
getShineBlockchainUsage,
|
||||||
getShineUsersEconomyConfig,
|
getShineUsersEconomyConfig,
|
||||||
updateShineUserPdaOnSolana,
|
updateShineUserPdaOnSolana,
|
||||||
} from '../services/shine-blockchain-wallet-service.js?v=2026052803';
|
} from '../services/shine-blockchain-wallet-service.js?v=202605300007';
|
||||||
|
|
||||||
export const pageMeta = { id: 'wallet-view', title: 'Кошелёк' };
|
export const pageMeta = { id: 'wallet-view', title: 'Кошелёк' };
|
||||||
const SOLANA_PRIVATE_BASE58_MAX_LEN = 44;
|
const SOLANA_PRIVATE_BASE58_MAX_LEN = 44;
|
||||||
@ -159,14 +159,14 @@ export function render({ navigate }) {
|
|||||||
|
|
||||||
const usedLabel = document.createElement('p');
|
const usedLabel = document.createElement('p');
|
||||||
usedLabel.className = 'meta-muted';
|
usedLabel.className = 'meta-muted';
|
||||||
usedLabel.textContent = 'Израсходовано (закреплено в Solana)';
|
usedLabel.textContent = 'Израсходовано (фактически на сервере)';
|
||||||
const usedValue = document.createElement('h2');
|
const usedValue = document.createElement('h2');
|
||||||
usedValue.style.fontSize = '26px';
|
usedValue.style.fontSize = '26px';
|
||||||
usedValue.textContent = '— KB';
|
usedValue.textContent = '— KB';
|
||||||
|
|
||||||
const leftLabel = document.createElement('p');
|
const leftLabel = document.createElement('p');
|
||||||
leftLabel.className = 'meta-muted';
|
leftLabel.className = 'meta-muted';
|
||||||
leftLabel.textContent = 'Осталось (по Solana)';
|
leftLabel.textContent = 'Осталось';
|
||||||
const leftValue = document.createElement('h2');
|
const leftValue = document.createElement('h2');
|
||||||
leftValue.style.fontSize = '30px';
|
leftValue.style.fontSize = '30px';
|
||||||
leftValue.textContent = '— KB';
|
leftValue.textContent = '— KB';
|
||||||
@ -185,11 +185,8 @@ export function render({ navigate }) {
|
|||||||
updatedLabel.textContent = 'Обновлено: —';
|
updatedLabel.textContent = 'Обновлено: —';
|
||||||
|
|
||||||
const serverTitle = document.createElement('h3');
|
const serverTitle = document.createElement('h3');
|
||||||
serverTitle.style.margin = '8px 0 0';
|
serverTitle.style.margin = '14px 0 0';
|
||||||
serverTitle.textContent = 'Фактическое состояние на сервере';
|
serverTitle.textContent = 'Фактическое состояние на сервере';
|
||||||
const serverBlocksLabel = document.createElement('p');
|
|
||||||
serverBlocksLabel.className = 'meta-muted';
|
|
||||||
serverBlocksLabel.textContent = 'Блоков: —';
|
|
||||||
const serverSizeLabel = document.createElement('p');
|
const serverSizeLabel = document.createElement('p');
|
||||||
serverSizeLabel.className = 'meta-muted';
|
serverSizeLabel.className = 'meta-muted';
|
||||||
serverSizeLabel.textContent = 'Размер цепочки: —';
|
serverSizeLabel.textContent = 'Размер цепочки: —';
|
||||||
@ -199,20 +196,19 @@ export function render({ navigate }) {
|
|||||||
const serverLastHashLabel = document.createElement('p');
|
const serverLastHashLabel = document.createElement('p');
|
||||||
serverLastHashLabel.className = 'meta-muted';
|
serverLastHashLabel.className = 'meta-muted';
|
||||||
serverLastHashLabel.style.wordBreak = 'break-all';
|
serverLastHashLabel.style.wordBreak = 'break-all';
|
||||||
|
serverLastHashLabel.style.fontSize = '11px';
|
||||||
serverLastHashLabel.textContent = 'Hash: —';
|
serverLastHashLabel.textContent = 'Hash: —';
|
||||||
|
|
||||||
const solanaTitle = document.createElement('h3');
|
const solanaTitle = document.createElement('h3');
|
||||||
solanaTitle.style.margin = '8px 0 0';
|
solanaTitle.style.margin = '14px 0 0';
|
||||||
solanaTitle.textContent = 'Закреплено в Solana';
|
solanaTitle.textContent = 'Закреплено в Solana';
|
||||||
const solanaBlocksLabel = document.createElement('p');
|
|
||||||
solanaBlocksLabel.className = 'meta-muted';
|
|
||||||
solanaBlocksLabel.textContent = 'Блоков: —';
|
|
||||||
const solanaLastLabel = document.createElement('p');
|
const solanaLastLabel = document.createElement('p');
|
||||||
solanaLastLabel.className = 'meta-muted';
|
solanaLastLabel.className = 'meta-muted';
|
||||||
solanaLastLabel.textContent = 'Крайний блок: —';
|
solanaLastLabel.textContent = 'Крайний блок: —';
|
||||||
const solanaLastHashLabel = document.createElement('p');
|
const solanaLastHashLabel = document.createElement('p');
|
||||||
solanaLastHashLabel.className = 'meta-muted';
|
solanaLastHashLabel.className = 'meta-muted';
|
||||||
solanaLastHashLabel.style.wordBreak = 'break-all';
|
solanaLastHashLabel.style.wordBreak = 'break-all';
|
||||||
|
solanaLastHashLabel.style.fontSize = '11px';
|
||||||
solanaLastHashLabel.textContent = 'Hash: —';
|
solanaLastHashLabel.textContent = 'Hash: —';
|
||||||
|
|
||||||
card.append(
|
card.append(
|
||||||
@ -226,12 +222,10 @@ export function render({ navigate }) {
|
|||||||
endpointLabel,
|
endpointLabel,
|
||||||
updatedLabel,
|
updatedLabel,
|
||||||
serverTitle,
|
serverTitle,
|
||||||
serverBlocksLabel,
|
|
||||||
serverSizeLabel,
|
serverSizeLabel,
|
||||||
serverLastLabel,
|
serverLastLabel,
|
||||||
serverLastHashLabel,
|
serverLastHashLabel,
|
||||||
solanaTitle,
|
solanaTitle,
|
||||||
solanaBlocksLabel,
|
|
||||||
solanaLastLabel,
|
solanaLastLabel,
|
||||||
solanaLastHashLabel,
|
solanaLastHashLabel,
|
||||||
);
|
);
|
||||||
@ -250,11 +244,11 @@ export function render({ navigate }) {
|
|||||||
const fetchServerState = async () => {
|
const fetchServerState = async () => {
|
||||||
const user = await authService.getUser(String(state.session.login || '').trim());
|
const user = await authService.getUser(String(state.session.login || '').trim());
|
||||||
if (!user?.exists) throw new Error('Пользователь не найден на сервере');
|
if (!user?.exists) throw new Error('Пользователь не найден на сервере');
|
||||||
|
const lastNumber = Number(user.serverLastGlobalNumber ?? -1);
|
||||||
return {
|
return {
|
||||||
blocksCount: Number(user.serverBlocksCount || 0),
|
|
||||||
sizeBytes: Number(user.serverBlockchainSizeBytes || 0),
|
sizeBytes: Number(user.serverBlockchainSizeBytes || 0),
|
||||||
sizeLimitBytes: Number(user.serverBlockchainSizeLimitBytes || 0),
|
sizeLimitBytes: Number(user.serverBlockchainSizeLimitBytes || 0),
|
||||||
lastNumber: Number(user.serverLastGlobalNumber ?? -1),
|
lastNumber,
|
||||||
lastHash: String(user.serverLastGlobalHash || ''),
|
lastHash: String(user.serverLastGlobalHash || ''),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -311,22 +305,19 @@ export function render({ navigate }) {
|
|||||||
fetchServerState(),
|
fetchServerState(),
|
||||||
]);
|
]);
|
||||||
if (modeToken !== activeModeToken) return;
|
if (modeToken !== activeModeToken) return;
|
||||||
const solanaBlocks = usage.lastBlockNumber >= 0 ? usage.lastBlockNumber + 1 : 0;
|
|
||||||
const serverBlocks = serverState.blocksCount;
|
|
||||||
|
|
||||||
limitValue.textContent = formatKbFromBytes(usage.paidLimitBytes);
|
limitValue.textContent = formatKbFromBytes(usage.paidLimitBytes);
|
||||||
usedValue.textContent = formatKbFromBytes(usage.usedBytes);
|
const usedServerBytes = BigInt(Math.max(0, Number(serverState.sizeBytes || 0)));
|
||||||
leftValue.textContent = formatKbFromBytes(usage.leftBytes);
|
const leftServerBytes = usage.paidLimitBytes > usedServerBytes ? (usage.paidLimitBytes - usedServerBytes) : 0n;
|
||||||
|
usedValue.textContent = formatKbFromBytes(usedServerBytes);
|
||||||
|
leftValue.textContent = formatKbFromBytes(leftServerBytes);
|
||||||
pdaLabel.textContent = `PDA: ${usage.userPda}`;
|
pdaLabel.textContent = `PDA: ${usage.userPda}`;
|
||||||
endpointLabel.textContent = `RPC: ${usage.endpoint}`;
|
endpointLabel.textContent = `RPC: ${usage.endpoint}`;
|
||||||
updatedLabel.textContent = `Обновлено: ${nowRu()}`;
|
updatedLabel.textContent = `Обновлено: ${nowRu()}`;
|
||||||
|
|
||||||
serverBlocksLabel.textContent = `Блоков: ${serverBlocks.toLocaleString('ru-RU')}`;
|
serverSizeLabel.textContent = `Размер цепочки: ${formatKbFromBytes(serverState.sizeBytes)}`;
|
||||||
serverSizeLabel.textContent = `Размер цепочки: ${formatKbFromBytes(serverState.sizeBytes)} из ${formatKbFromBytes(serverState.sizeLimitBytes)}`;
|
|
||||||
serverLastLabel.textContent = `Крайний блок: ${serverState.lastNumber}`;
|
serverLastLabel.textContent = `Крайний блок: ${serverState.lastNumber}`;
|
||||||
serverLastHashLabel.textContent = `Hash: ${serverState.lastHash || '—'}`;
|
serverLastHashLabel.textContent = `Hash: ${serverState.lastHash || '—'}`;
|
||||||
|
|
||||||
solanaBlocksLabel.textContent = `Блоков: ${solanaBlocks.toLocaleString('ru-RU')}`;
|
|
||||||
solanaLastLabel.textContent = `Крайний блок: ${usage.lastBlockNumber}`;
|
solanaLastLabel.textContent = `Крайний блок: ${usage.lastBlockNumber}`;
|
||||||
solanaLastHashLabel.textContent = `Hash: ${usage.lastBlockHashHex || '—'}`;
|
solanaLastHashLabel.textContent = `Hash: ${usage.lastBlockHashHex || '—'}`;
|
||||||
|
|
||||||
@ -410,7 +401,7 @@ export function render({ navigate }) {
|
|||||||
} finally {
|
} finally {
|
||||||
setButtonsDisabled(false);
|
setButtonsDisabled(false);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
refreshBtn.addEventListener('click', () => {
|
refreshBtn.addEventListener('click', () => {
|
||||||
void refreshUsage();
|
void refreshUsage();
|
||||||
|
|||||||
@ -457,10 +457,12 @@ export async function updateShineUserPdaOnSolana({
|
|||||||
],
|
],
|
||||||
data: ixData,
|
data: ixData,
|
||||||
});
|
});
|
||||||
|
const computeIx = solana.ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 });
|
||||||
|
const heapIx = solana.ComputeBudgetProgram.requestHeapFrame({ bytes: 131_072 });
|
||||||
|
|
||||||
const signature = await solana.sendAndConfirmTransaction(
|
const signature = await solana.sendAndConfirmTransaction(
|
||||||
connection,
|
connection,
|
||||||
new solana.Transaction().add(edIxRoot, edIxBch, updIx),
|
new solana.Transaction().add(computeIx, heapIx, edIxRoot, edIxBch, updIx),
|
||||||
[deviceKeypair],
|
[deviceKeypair],
|
||||||
{ commitment: 'confirmed' },
|
{ commitment: 'confirmed' },
|
||||||
);
|
);
|
||||||
|
|||||||
@ -91,7 +91,6 @@ public class Net_GetUser_Handler implements JsonMessageHandler {
|
|||||||
resp.setServerLastGlobalHash(toHex32(lastHash));
|
resp.setServerLastGlobalHash(toHex32(lastHash));
|
||||||
resp.setServerBlockchainSizeBytes(st.getFileSizeBytes());
|
resp.setServerBlockchainSizeBytes(st.getFileSizeBytes());
|
||||||
resp.setServerBlockchainSizeLimitBytes(st.getSizeLimit());
|
resp.setServerBlockchainSizeLimitBytes(st.getSizeLimit());
|
||||||
resp.setServerBlocksCount(Math.max(0, lastNum + 1));
|
|
||||||
|
|
||||||
log.info("✅ GetUser: found login={}, blockchainName={}", u.getLogin(), u.getBlockchainName());
|
log.info("✅ GetUser: found login={}, blockchainName={}", u.getLogin(), u.getBlockchainName());
|
||||||
return resp;
|
return resp;
|
||||||
|
|||||||
@ -43,7 +43,6 @@ public class Net_GetUser_Response extends Net_Response {
|
|||||||
private String serverLastGlobalHash;
|
private String serverLastGlobalHash;
|
||||||
private Long serverBlockchainSizeBytes;
|
private Long serverBlockchainSizeBytes;
|
||||||
private Long serverBlockchainSizeLimitBytes;
|
private Long serverBlockchainSizeLimitBytes;
|
||||||
private Integer serverBlocksCount;
|
|
||||||
|
|
||||||
public Boolean getExists() { return exists; }
|
public Boolean getExists() { return exists; }
|
||||||
public void setExists(Boolean exists) { this.exists = exists; }
|
public void setExists(Boolean exists) { this.exists = exists; }
|
||||||
@ -75,6 +74,4 @@ public class Net_GetUser_Response extends Net_Response {
|
|||||||
public Long getServerBlockchainSizeLimitBytes() { return serverBlockchainSizeLimitBytes; }
|
public Long getServerBlockchainSizeLimitBytes() { return serverBlockchainSizeLimitBytes; }
|
||||||
public void setServerBlockchainSizeLimitBytes(Long serverBlockchainSizeLimitBytes) { this.serverBlockchainSizeLimitBytes = serverBlockchainSizeLimitBytes; }
|
public void setServerBlockchainSizeLimitBytes(Long serverBlockchainSizeLimitBytes) { this.serverBlockchainSizeLimitBytes = serverBlockchainSizeLimitBytes; }
|
||||||
|
|
||||||
public Integer getServerBlocksCount() { return serverBlocksCount; }
|
|
||||||
public void setServerBlocksCount(Integer serverBlocksCount) { this.serverBlocksCount = serverBlocksCount; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
5
tools/understand-anything-lab/.gitignore
vendored
Normal file
5
tools/understand-anything-lab/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
upstream/
|
||||||
|
work/
|
||||||
|
.understand-anything/
|
||||||
|
*.log
|
||||||
|
|
||||||
65
tools/understand-anything-lab/README.md
Normal file
65
tools/understand-anything-lab/README.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# Эксперимент Understand Anything
|
||||||
|
|
||||||
|
Эта папка нужна, чтобы безопасно проверить `Lum1104/Understand-Anything` в SHiNE без подключения к сборке, деплою и рабочему серверному коду.
|
||||||
|
|
||||||
|
## Что здесь лежит
|
||||||
|
|
||||||
|
- `upstream/` - локальный клон `https://github.com/Lum1104/Understand-Anything.git`; он игнорируется git, чтобы не тащить внешний репозиторий внутрь SHiNE.
|
||||||
|
- `install_codex_skills.sh` - локальная установка skills для Codex из уже скачанного `upstream/`, без `curl | bash`.
|
||||||
|
|
||||||
|
Проверенный upstream:
|
||||||
|
|
||||||
|
- commit: `26edf61856fa476e466bda1814819a266a293c47`
|
||||||
|
- дата commit: `2026-05-26T13:09:57+08:00`
|
||||||
|
- сообщение: `Merge pull request #235 from ZebangCheng/feat/add-trae-platform`
|
||||||
|
|
||||||
|
## Безопасный сценарий проверки
|
||||||
|
|
||||||
|
1. Подготовить окружение:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --version
|
||||||
|
pnpm --version
|
||||||
|
```
|
||||||
|
|
||||||
|
Для полноценного запуска нужен Node.js 22 или новее и pnpm 10 или новее. На текущей машине при первичной проверке были обнаружены Node.js `v18.19.1` и отсутствие `pnpm`, поэтому полноценный `/understand` пока не запускался.
|
||||||
|
|
||||||
|
2. Установить skills для Codex из локального клона:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./tools/understand-anything-lab/install_codex_skills.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт использует локальный `tools/understand-anything-lab/upstream`, а не скачивает установщик из интернета.
|
||||||
|
|
||||||
|
3. Перезапустить Codex-сессию, чтобы появились команды/skills:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/understand --language ru
|
||||||
|
/understand-dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
4. После первого запуска проверить созданный файл:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -la .understand-anything/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что не делать без отдельного решения
|
||||||
|
|
||||||
|
- Не включать `Understand Anything` в Gradle-сборку.
|
||||||
|
- Не добавлять его в `deployServer` или `deployUI`.
|
||||||
|
- Не переносить серверные модули в новую папку одновременно с этим экспериментом.
|
||||||
|
- Не включать auto-update hook через `/understand --auto-update`, пока не понятно, нужен ли граф в каждом commit.
|
||||||
|
|
||||||
|
## Что можно коммитить позже
|
||||||
|
|
||||||
|
Если эксперимент подтвердится, можно будет отдельно решить, нужно ли хранить `.understand-anything/knowledge-graph.json` в репозитории. По рекомендации upstream обычно коммитят содержимое `.understand-anything/`, кроме:
|
||||||
|
|
||||||
|
```gitignore
|
||||||
|
.understand-anything/intermediate/
|
||||||
|
.understand-anything/diff-overlay.json
|
||||||
|
```
|
||||||
|
|
||||||
|
До такого решения артефакты графа лучше считать локальными.
|
||||||
|
|
||||||
29
tools/understand-anything-lab/install_codex_skills.sh
Executable file
29
tools/understand-anything-lab/install_codex_skills.sh
Executable file
@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||||
|
UPSTREAM_DIR="$SCRIPT_DIR/upstream"
|
||||||
|
|
||||||
|
if [[ ! -d "$UPSTREAM_DIR/.git" ]]; then
|
||||||
|
echo "Не найден локальный upstream-клон: $UPSTREAM_DIR" >&2
|
||||||
|
echo "Сначала выполните:" >&2
|
||||||
|
echo " git clone --depth 1 https://github.com/Lum1104/Understand-Anything.git \"$UPSTREAM_DIR\"" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v node >/dev/null 2>&1; then
|
||||||
|
echo "Не найден Node.js. Для полноценного запуска Understand Anything нужен Node.js 22+." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v pnpm >/dev/null 2>&1; then
|
||||||
|
echo "Не найден pnpm. Для полноценного запуска Understand Anything нужен pnpm 10+." >&2
|
||||||
|
echo "Skills можно установить, но команда /understand не соберёт core без pnpm." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
UA_DIR="$UPSTREAM_DIR" "$UPSTREAM_DIR/install.sh" codex
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Skills Understand Anything установлены для Codex из локального клона."
|
||||||
|
echo "Перезапустите Codex-сессию перед использованием /understand."
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user