@danhachuel/thunderbolt 0.3.95 → 0.3.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/main.py +50 -3
- package/hermes_ui/domain.py +31 -0
- package/hermes_ui/update_manager.py +108 -0
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -4,6 +4,7 @@ import hashlib
|
|
|
4
4
|
import json
|
|
5
5
|
import mimetypes
|
|
6
6
|
import re
|
|
7
|
+
import time
|
|
7
8
|
from contextlib import nullcontext
|
|
8
9
|
from datetime import date, datetime, timezone
|
|
9
10
|
import sys
|
|
@@ -22,7 +23,7 @@ try:
|
|
|
22
23
|
except (OSError, json.JSONDecodeError):
|
|
23
24
|
APP_VERSION = ""
|
|
24
25
|
|
|
25
|
-
from hermes_ui.domain import STAGES, create_batch, create_channel, create_tasks_for_batch, delete_channel, delete_task, pipeline_summary, set_channel_defaults, transition_task, update_channel, update_channel_video
|
|
26
|
+
from hermes_ui.domain import STAGES, create_batch, create_channel, create_tasks_for_batch, delete_channel, delete_task, pipeline_summary, retry_task_with_current_settings, set_channel_defaults, transition_task, update_channel, update_channel_video
|
|
26
27
|
from hermes_ui.drafts import list_drafts, save_draft
|
|
27
28
|
from hermes_ui.automation_worker import load_worker_status
|
|
28
29
|
from hermes_ui.pipeline_worker import load_pipeline_worker_status, recover_stale_tasks, STALE_TASK_SECONDS, WORKER_HEARTBEAT_TIMEOUT_SECONDS
|
|
@@ -49,6 +50,7 @@ from hermes_ui.logs import list_logs, logs_to_rows
|
|
|
49
50
|
from hermes_ui.languages import LANGUAGE_CODES, VIDEO_LANGUAGE_CODES, LANGUAGE_FLAG_DATA_URIS, language_code, language_label, ui_language_menu_label, ui_text, video_language_label, video_language_options
|
|
50
51
|
from hermes_ui.api_key_tests import test_apify_credentials, test_influencer_database, test_innertube_api_key, test_kaggle_credentials, test_material_source_credentials, test_media_provider_card, test_nano_banana_credentials, test_postiz_credentials, test_telegram_credentials, test_tiktok_credentials, test_upload_post_credentials, test_voice_provider
|
|
51
52
|
from hermes_ui.tutorials import tutorial_body, tutorial_caption, tutorial_title
|
|
53
|
+
from hermes_ui.update_manager import check_version, update_to_latest
|
|
52
54
|
|
|
53
55
|
from hermes_ui.script_documents import list_script_documents, read_script_document, save_script_document, script_storage_path
|
|
54
56
|
from hermes_ui.script_generation import generate_script_document
|
|
@@ -1269,6 +1271,48 @@ def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], yout
|
|
|
1269
1271
|
|
|
1270
1272
|
def render_dashboard():
|
|
1271
1273
|
ui_language = current_ui_language()
|
|
1274
|
+
update_area, version_area = st.columns([1.45, 4.55])
|
|
1275
|
+
with update_area:
|
|
1276
|
+
st.markdown(
|
|
1277
|
+
"""
|
|
1278
|
+
<style>
|
|
1279
|
+
div[data-testid="stButton"] button[kind="primary"] {
|
|
1280
|
+
background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);
|
|
1281
|
+
color: #ffffff;
|
|
1282
|
+
border: 1px solid #8b5cf6;
|
|
1283
|
+
font-weight: 700;
|
|
1284
|
+
box-shadow: 0 8px 20px rgba(79, 70, 229, 0.28);
|
|
1285
|
+
}
|
|
1286
|
+
div[data-testid="stButton"] button[kind="primary"]:hover {
|
|
1287
|
+
border-color: #c4b5fd;
|
|
1288
|
+
filter: brightness(1.08);
|
|
1289
|
+
}
|
|
1290
|
+
</style>
|
|
1291
|
+
""",
|
|
1292
|
+
unsafe_allow_html=True,
|
|
1293
|
+
)
|
|
1294
|
+
if st.button("Atualizar Versão", key="home_update_version", use_container_width=True, type="primary", icon=":material/system_update:"):
|
|
1295
|
+
with st.spinner("A instalar a versão mais recente…"):
|
|
1296
|
+
st.session_state["home_update_result"] = update_to_latest(APP_VERSION)
|
|
1297
|
+
with version_area:
|
|
1298
|
+
cache_key = "home_update_version_check"
|
|
1299
|
+
checked_at_key = "home_update_version_checked_at"
|
|
1300
|
+
if not st.session_state.get(cache_key) or time.monotonic() - float(st.session_state.get(checked_at_key, 0)) > 300:
|
|
1301
|
+
st.session_state[cache_key] = check_version(APP_VERSION)
|
|
1302
|
+
st.session_state[checked_at_key] = time.monotonic()
|
|
1303
|
+
version_status = st.session_state[cache_key]
|
|
1304
|
+
if version_status.update_available:
|
|
1305
|
+
st.info(f"Nova versão disponível: {version_status.latest_version}. A versão actual é {APP_VERSION or 'desconhecida'}.")
|
|
1306
|
+
elif version_status.error:
|
|
1307
|
+
st.caption(f"Versão actual: {APP_VERSION or 'desconhecida'} · verificação de actualização indisponível.")
|
|
1308
|
+
else:
|
|
1309
|
+
st.caption(f"Versão actual: {APP_VERSION or 'desconhecida'} · já está actualizada ({version_status.latest_version}).")
|
|
1310
|
+
update_result = st.session_state.get("home_update_result")
|
|
1311
|
+
if update_result is not None:
|
|
1312
|
+
if update_result.ok:
|
|
1313
|
+
st.success(update_result.message)
|
|
1314
|
+
else:
|
|
1315
|
+
st.error(update_result.message)
|
|
1272
1316
|
st.title("Thunderbolt")
|
|
1273
1317
|
st.caption(ui_text("Interface local para operação e automação de conteúdo faceless", ui_language))
|
|
1274
1318
|
summary = pipeline_summary()
|
|
@@ -4054,7 +4098,7 @@ def render_automation():
|
|
|
4054
4098
|
|
|
4055
4099
|
st.divider()
|
|
4056
4100
|
st.subheader("Vídeos cadastrados")
|
|
4057
|
-
st.caption("Start retoma as etapas já concluídas e só gera novamente o que ainda não estiver pronto. Apagar remove o card da fila após confirmação e preserva os artefactos locais.")
|
|
4101
|
+
st.caption("Start retoma as etapas já concluídas e só gera novamente o que ainda não estiver pronto. Em tarefas falhadas ou bloqueadas, a nova tentativa lê as chaves, prioridades e configurações actualmente guardadas. Apagar remove o card da fila após confirmação e preserva os artefactos locais.")
|
|
4058
4102
|
tasks = load_video_tasks_for_catalog()
|
|
4059
4103
|
if not tasks:
|
|
4060
4104
|
st.info("Ainda não existem vídeos cadastrados.")
|
|
@@ -4074,7 +4118,10 @@ def render_automation():
|
|
|
4074
4118
|
start_col, stop_col, delete_col = st.columns(3)
|
|
4075
4119
|
with start_col:
|
|
4076
4120
|
if st.button("Start", key=f"automation_start_{task['id']}", use_container_width=True, disabled=state not in {"to_do", "blocked", "failed"}):
|
|
4077
|
-
|
|
4121
|
+
if state in {"failed", "blocked"}:
|
|
4122
|
+
retry_task_with_current_settings(task["id"])
|
|
4123
|
+
else:
|
|
4124
|
+
transition_task(task["id"], "doing")
|
|
4078
4125
|
st.rerun()
|
|
4079
4126
|
with stop_col:
|
|
4080
4127
|
if st.button("Stop", key=f"automation_stop_{task['id']}", use_container_width=True, disabled=state != "doing"):
|
package/hermes_ui/domain.py
CHANGED
|
@@ -328,6 +328,37 @@ def transition_task(task_id: str, state: str | None = None, stage: str | None =
|
|
|
328
328
|
return None
|
|
329
329
|
|
|
330
330
|
|
|
331
|
+
def retry_task_with_current_settings(task_id: str) -> dict[str, Any] | None:
|
|
332
|
+
"""Queue a failed or blocked task without persisting API credentials or provider snapshots.
|
|
333
|
+
|
|
334
|
+
The pipeline reloads settings.json immediately before every execution, so a
|
|
335
|
+
retry always uses the currently saved API keys, active provider priorities,
|
|
336
|
+
and provider endpoints while retaining the task's completed artefacts.
|
|
337
|
+
"""
|
|
338
|
+
tasks = read_json("tasks.json", [])
|
|
339
|
+
for task in tasks:
|
|
340
|
+
if task.get("id") != task_id:
|
|
341
|
+
continue
|
|
342
|
+
previous_state = str(task.get("state") or "")
|
|
343
|
+
if previous_state not in {"failed", "blocked"}:
|
|
344
|
+
raise ValueError("Apenas tarefas falhadas ou bloqueadas podem ser retomadas.")
|
|
345
|
+
try:
|
|
346
|
+
retry_count = int(task.get("retry_count") or 0)
|
|
347
|
+
except (TypeError, ValueError):
|
|
348
|
+
retry_count = 0
|
|
349
|
+
task["state"] = "to_do"
|
|
350
|
+
task["error"] = None
|
|
351
|
+
task["retry_count"] = retry_count + 1
|
|
352
|
+
task["retry_requested_at"] = now()
|
|
353
|
+
task["retry_config_source"] = "settings.json_at_execution"
|
|
354
|
+
for field in ("failure_api", "failure_provider", "failure_service", "failure_config_fields"):
|
|
355
|
+
task.pop(field, None)
|
|
356
|
+
task["updated_at"] = now()
|
|
357
|
+
write_json("tasks.json", tasks)
|
|
358
|
+
return task
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
|
|
331
362
|
def pipeline_summary() -> dict[str, Any]:
|
|
332
363
|
tasks = read_json("tasks.json", [])
|
|
333
364
|
channels = read_json("channels.json", [])
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Verificação e actualização local da versão distribuída pelo NPM.
|
|
2
|
+
|
|
3
|
+
O módulo não recebe nem manipula credenciais. A instalação só é iniciada após o
|
|
4
|
+
clique explícito do utilizador na interface local.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Callable
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
PACKAGE_NAME = "@danhachuel/thunderbolt"
|
|
18
|
+
REGISTRY_URL = "https://registry.npmjs.org/@danhachuel/thunderbolt/latest"
|
|
19
|
+
UPDATE_TIMEOUT_SECONDS = 20 * 60
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class VersionCheck:
|
|
24
|
+
"""Version information displayed on the local home page."""
|
|
25
|
+
|
|
26
|
+
current_version: str
|
|
27
|
+
latest_version: str = ""
|
|
28
|
+
error: str = ""
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def update_available(self) -> bool:
|
|
32
|
+
return bool(self.latest_version and self.current_version and self.latest_version != self.current_version)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class UpdateResult:
|
|
37
|
+
"""Sanitised result of an explicit local package update request."""
|
|
38
|
+
|
|
39
|
+
ok: bool
|
|
40
|
+
latest_version: str = ""
|
|
41
|
+
message: str = ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def latest_package_version(*, timeout: int = 8, get: Callable[..., Any] = requests.get) -> str:
|
|
45
|
+
"""Return the latest public package version without sending local configuration."""
|
|
46
|
+
response = get(REGISTRY_URL, timeout=timeout)
|
|
47
|
+
response.raise_for_status()
|
|
48
|
+
payload = response.json()
|
|
49
|
+
version = str(payload.get("version") or "").strip() if isinstance(payload, dict) else ""
|
|
50
|
+
if not version:
|
|
51
|
+
raise ValueError("O registry NPM não devolveu uma versão válida.")
|
|
52
|
+
return version
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def check_version(current_version: str, *, timeout: int = 8, get: Callable[..., Any] = requests.get) -> VersionCheck:
|
|
56
|
+
"""Fetch only the package metadata needed for the version badge."""
|
|
57
|
+
current = str(current_version or "").strip()
|
|
58
|
+
try:
|
|
59
|
+
return VersionCheck(current_version=current, latest_version=latest_package_version(timeout=timeout, get=get))
|
|
60
|
+
except (requests.RequestException, ValueError) as exc:
|
|
61
|
+
return VersionCheck(current_version=current, error=f"Não foi possível verificar actualizações agora ({type(exc).__name__}).")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def update_command() -> list[str]:
|
|
65
|
+
"""Build the same cross-platform install command documented for Thunderbolt."""
|
|
66
|
+
executable = "npx.cmd" if os.name == "nt" else "npx"
|
|
67
|
+
return [executable, "--yes", "--prefer-online", PACKAGE_NAME, "install"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def update_to_latest(
|
|
71
|
+
current_version: str,
|
|
72
|
+
*,
|
|
73
|
+
timeout: int = UPDATE_TIMEOUT_SECONDS,
|
|
74
|
+
get: Callable[..., Any] = requests.get,
|
|
75
|
+
run: Callable[..., Any] = subprocess.run,
|
|
76
|
+
) -> UpdateResult:
|
|
77
|
+
"""Install the latest package only after an explicit UI action.
|
|
78
|
+
|
|
79
|
+
The running local process keeps its current code until it is restarted. No
|
|
80
|
+
subprocess output is returned to the UI, preventing accidental display of
|
|
81
|
+
environment values from third-party installers.
|
|
82
|
+
"""
|
|
83
|
+
status = check_version(current_version, get=get)
|
|
84
|
+
if status.error:
|
|
85
|
+
return UpdateResult(False, message=status.error)
|
|
86
|
+
if not status.update_available:
|
|
87
|
+
return UpdateResult(True, latest_version=status.latest_version, message="O Thunderbolt já está na versão mais recente.")
|
|
88
|
+
try:
|
|
89
|
+
completed = run(
|
|
90
|
+
update_command(),
|
|
91
|
+
stdin=subprocess.DEVNULL,
|
|
92
|
+
stdout=subprocess.DEVNULL,
|
|
93
|
+
stderr=subprocess.DEVNULL,
|
|
94
|
+
timeout=timeout,
|
|
95
|
+
check=False,
|
|
96
|
+
)
|
|
97
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
98
|
+
return UpdateResult(False, latest_version=status.latest_version, message=f"Não foi possível concluir a actualização ({type(exc).__name__}).")
|
|
99
|
+
if int(getattr(completed, "returncode", 1)) != 0:
|
|
100
|
+
return UpdateResult(False, latest_version=status.latest_version, message="A actualização não foi concluída. Feche processos Thunderbolt em execução e tente novamente.")
|
|
101
|
+
return UpdateResult(
|
|
102
|
+
True,
|
|
103
|
+
latest_version=status.latest_version,
|
|
104
|
+
message=(
|
|
105
|
+
f"A versão {status.latest_version} foi instalada. Reinicie o Thunderbolt para abrir a versão nova; "
|
|
106
|
+
"os dados e configurações locais foram preservados."
|
|
107
|
+
),
|
|
108
|
+
)
|
package/package.json
CHANGED