@danhachuel/thunderbolt 0.4.62 → 0.4.64
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/MANUAL-INSTALACAO.md +2 -0
- package/app/main.py +26 -9
- package/hermes_ui/storage.py +3 -0
- package/integrations/upload_routing.py +19 -4
- package/package.json +1 -1
package/MANUAL-INSTALACAO.md
CHANGED
|
@@ -839,3 +839,5 @@ Antes de clicar em **Enviar vídeo via Composio**, confirme o vídeo, o toolkit,
|
|
|
839
839
|
Na **Automação Youtube**, Composio é a rota padrão quando a integração está activa e tem API key, slug da ferramenta e campo do ficheiro configurados. A ordem automática passa a ser **Composio → API Oficial → Upload directo → Postiz**. Se Composio falhar ou não estiver configurado, o worker tenta os fallbacks disponíveis e mantém o comportamento de publicação existente.
|
|
840
840
|
|
|
841
841
|
Para garantir o canal correcto, configure também o **Campo do canal na ferramenta**. O worker lê o `youtube_channel_id` do canal associado à tarefa e substitui explicitamente esse campo nos argumentos enviados ao Composio. Se a tarefa não tiver um YouTube channel ID sincronizado, ou se os argumentos configurarem outro ID, o envio Composio é bloqueado e os fallbacks podem actuar; a publicação nunca é enviada silenciosamente para um canal diferente.
|
|
842
|
+
|
|
843
|
+
Na Automação Youtube, o Composio recebe ainda os mesmos parâmetros do upload oficial: privacidade `unlisted` (Não listado), categoria `22` e o locale do idioma do vídeo/canal, como `pt-BR`. Os nomes dos campos podem ser ajustados no painel, mas valores divergentes nos argumentos JSON são bloqueados.
|
package/app/main.py
CHANGED
|
@@ -2109,15 +2109,29 @@ def render_channels():
|
|
|
2109
2109
|
col1, col2 = st.columns([1, 1])
|
|
2110
2110
|
with col1:
|
|
2111
2111
|
if st.button("Buscar no YouTube", type="primary", use_container_width=True, key="youtube_channel_lookup"):
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2112
|
+
search_source = str(source or "").strip()
|
|
2113
|
+
st.session_state.pop("yt_import", None)
|
|
2114
|
+
st.session_state.pop("yt_message", None)
|
|
2115
|
+
st.session_state.pop("yt_ok", None)
|
|
2116
|
+
if not search_source:
|
|
2117
|
+
st.session_state["yt_message"] = "Introduza um URL youtube.com, um handle @nome ou um ID de canal UC... antes de pesquisar."
|
|
2118
|
+
st.session_state["yt_ok"] = False
|
|
2116
2119
|
else:
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2120
|
+
try:
|
|
2121
|
+
with st.spinner("A pesquisar o canal no YouTube…"):
|
|
2122
|
+
if lookup_mode.startswith("Página pública"):
|
|
2123
|
+
result = youtube.fetch_channel_public(search_source)
|
|
2124
|
+
elif not youtube.api_key:
|
|
2125
|
+
result = IntegrationResult(False, "A YouTube Data API Key não está configurada. Escolha a opção Página pública — sem API Key ou configure a chave em Configurações > Contas Google.", {"status": "api_key_not_configured"})
|
|
2126
|
+
else:
|
|
2127
|
+
result = youtube.fetch_channel(search_source)
|
|
2128
|
+
st.session_state["yt_import"] = result.data if isinstance(result.data, dict) else {}
|
|
2129
|
+
st.session_state["yt_message"] = result.message
|
|
2130
|
+
st.session_state["yt_ok"] = result.ok
|
|
2131
|
+
except Exception as exc:
|
|
2132
|
+
st.session_state["yt_import"] = {}
|
|
2133
|
+
st.session_state["yt_message"] = f"A pesquisa do YouTube falhou ({type(exc).__name__}). Confirme o URL/handle e tente novamente. Detalhe: {str(exc)[:240]}"
|
|
2134
|
+
st.session_state["yt_ok"] = False
|
|
2121
2135
|
with col2:
|
|
2122
2136
|
if st.button("Limpar importação", use_container_width=True, key="youtube_channel_clear"):
|
|
2123
2137
|
for key in ("yt_import", "yt_message", "yt_ok"):
|
|
@@ -7192,6 +7206,9 @@ def render_settings():
|
|
|
7192
7206
|
composio_tool_slug = st.text_input("Slug da ferramenta para Automação Youtube", value=str(settings.get("composio_tool_slug") or ""), key="upload_composio_tool_slug", help="Depois de descobrir uma ferramenta em Upload via Composio, copie aqui o slug para a Automação Youtube.")
|
|
7193
7207
|
composio_file_field = st.text_input("Campo do ficheiro na ferramenta", value=str(settings.get("composio_file_field") or "file"), key="upload_composio_file_field")
|
|
7194
7208
|
composio_channel_field = st.text_input("Campo do canal na ferramenta", value=str(settings.get("composio_channel_field") or "channel_id"), key="upload_composio_channel_field", help="O worker substitui este campo pelo YouTube channel ID do canal da tarefa, impedindo o envio para outro canal.")
|
|
7209
|
+
composio_privacy_field = st.text_input("Campo de privacidade", value=str(settings.get("composio_privacy_field") or "privacy_status"), key="upload_composio_privacy_field", help="Recebe sempre `unlisted` (Não listado), como no upload oficial.")
|
|
7210
|
+
composio_category_field = st.text_input("Campo de categoria", value=str(settings.get("composio_category_field") or "category_id"), key="upload_composio_category_field", help="Recebe sempre `22`, categoria Pessoas e blogs, como no upload oficial.")
|
|
7211
|
+
composio_language_field = st.text_input("Campo de idioma", value=str(settings.get("composio_language_field") or "language"), key="upload_composio_language_field", help="Recebe o locale do idioma do vídeo/canal, por exemplo `pt-BR`.")
|
|
7195
7212
|
composio_arguments_json = st.text_area("Argumentos JSON da Automação Youtube", value=str(settings.get("composio_arguments_json") or "{}"), key="upload_composio_arguments_json", height=120)
|
|
7196
7213
|
_render_credential_status(composio_api_key)
|
|
7197
7214
|
composio_action_cols = st.columns(2)
|
|
@@ -7200,7 +7217,7 @@ def render_settings():
|
|
|
7200
7217
|
with composio_action_cols[1]:
|
|
7201
7218
|
test_composio = st.button("Testar configuração", use_container_width=True, key="upload_composio_test")
|
|
7202
7219
|
if save_composio:
|
|
7203
|
-
settings.update({"composio_enabled": bool(composio_enabled), "composio_auto_upload": bool(composio_auto_upload), "composio_api_key": composio_api_key.strip(), "composio_user_id": composio_user_id.strip() or "thunderbolt-local", "composio_toolkit": composio_toolkit.strip(), "composio_tool_slug": composio_tool_slug.strip(), "composio_file_field": composio_file_field.strip() or "file", "composio_channel_field": composio_channel_field.strip() or "channel_id", "composio_arguments_json": composio_arguments_json.strip() or "{}"})
|
|
7220
|
+
settings.update({"composio_enabled": bool(composio_enabled), "composio_auto_upload": bool(composio_auto_upload), "composio_api_key": composio_api_key.strip(), "composio_user_id": composio_user_id.strip() or "thunderbolt-local", "composio_toolkit": composio_toolkit.strip(), "composio_tool_slug": composio_tool_slug.strip(), "composio_file_field": composio_file_field.strip() or "file", "composio_channel_field": composio_channel_field.strip() or "channel_id", "composio_privacy_field": composio_privacy_field.strip() or "privacy_status", "composio_category_field": composio_category_field.strip() or "category_id", "composio_language_field": composio_language_field.strip() or "language", "composio_arguments_json": composio_arguments_json.strip() or "{}"})
|
|
7204
7221
|
write_json("settings.json", settings)
|
|
7205
7222
|
st.success("Configuração Composio guardada.")
|
|
7206
7223
|
st.rerun()
|
package/hermes_ui/storage.py
CHANGED
|
@@ -295,6 +295,9 @@ DEFAULTS: dict[str, Any] = {
|
|
|
295
295
|
"composio_tool_slug": "",
|
|
296
296
|
"composio_file_field": "file",
|
|
297
297
|
"composio_channel_field": "channel_id",
|
|
298
|
+
"composio_privacy_field": "privacy_status",
|
|
299
|
+
"composio_category_field": "category_id",
|
|
300
|
+
"composio_language_field": "language",
|
|
298
301
|
"composio_arguments_json": "{}",
|
|
299
302
|
"postiz_enabled": False,
|
|
300
303
|
"postiz_api_key": "",
|
|
@@ -20,6 +20,7 @@ from integrations.youtube_direct_credentials import document_status
|
|
|
20
20
|
from integrations.youtube_direct_upload import YouTubeDirectUploader
|
|
21
21
|
from integrations.composio_upload import ComposioUploadError, execute_upload
|
|
22
22
|
from hermes_ui.storage import read_json, write_json
|
|
23
|
+
from hermes_ui.languages import language_locale
|
|
23
24
|
|
|
24
25
|
OFFICIAL_DAILY_LIMIT = 5
|
|
25
26
|
QUOTA_FILENAME = "official_upload_quota.json"
|
|
@@ -108,6 +109,9 @@ def upload_with_default_route(
|
|
|
108
109
|
settings,
|
|
109
110
|
channel=channel,
|
|
110
111
|
video_path=video_path,
|
|
112
|
+
category_id=category_id,
|
|
113
|
+
language=language or channel.get("language") or "pt-BR",
|
|
114
|
+
privacy_status=privacy_status or "unlisted",
|
|
111
115
|
title=title,
|
|
112
116
|
description=description,
|
|
113
117
|
tags=tags or [],
|
|
@@ -226,10 +230,21 @@ def _composio_upload(settings: dict[str, Any], *, channel: dict[str, Any], **kwa
|
|
|
226
230
|
parsed_arguments = json.loads(arguments)
|
|
227
231
|
if not isinstance(parsed_arguments, dict):
|
|
228
232
|
return IntegrationResult(False, "Composio não foi executado: os argumentos JSON devem ser um objecto.", {})
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
locked_values = {
|
|
234
|
+
channel_field: channel_id,
|
|
235
|
+
str(settings.get("composio_privacy_field") or "privacy_status").strip(): str(kwargs.get("privacy_status") or "unlisted"),
|
|
236
|
+
str(settings.get("composio_category_field") or "category_id").strip(): str(kwargs.get("category_id") or "22"),
|
|
237
|
+
str(settings.get("composio_language_field") or "language").strip(): language_locale(kwargs.get("language") or channel.get("language") or "pt"),
|
|
238
|
+
}
|
|
239
|
+
for field, expected in locked_values.items():
|
|
240
|
+
if not field:
|
|
241
|
+
return IntegrationResult(False, "Composio não foi executado: existe um campo obrigatório vazio na configuração.", {})
|
|
242
|
+
configured_value = parsed_arguments.get(field)
|
|
243
|
+
if configured_value not in (None, "", expected):
|
|
244
|
+
if field == channel_field:
|
|
245
|
+
return IntegrationResult(False, f"Composio bloqueado: o campo `{field}` aponta para outro canal.", {"field": field, "expected": expected})
|
|
246
|
+
return IntegrationResult(False, f"Composio bloqueado: o campo `{field}` tem um valor diferente do upload oficial ({expected}).", {"field": field, "expected": expected})
|
|
247
|
+
parsed_arguments[field] = expected
|
|
233
248
|
result = execute_upload(
|
|
234
249
|
str(settings.get("composio_api_key") or ""),
|
|
235
250
|
str(settings.get("composio_user_id") or ""),
|
package/package.json
CHANGED