@danhachuel/thunderbolt 0.4.63 → 0.4.65
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 +22 -5
- 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
|
@@ -2086,6 +2086,23 @@ def render_tiktok_channels():
|
|
|
2086
2086
|
update_channel(channel["id"], {"platform": "tiktok"})
|
|
2087
2087
|
st.success("Canal TikTok cadastrado.")
|
|
2088
2088
|
st.rerun()
|
|
2089
|
+
def is_youtube_channel_record(channel: Any) -> bool:
|
|
2090
|
+
"""Return whether a persisted channel belongs in the YouTube UI."""
|
|
2091
|
+
if not isinstance(channel, dict):
|
|
2092
|
+
return False
|
|
2093
|
+
platform = str(channel.get("platform") or "").strip().casefold().replace(" ", "")
|
|
2094
|
+
if platform in {"tiktok", "tik-tok"}:
|
|
2095
|
+
return False
|
|
2096
|
+
for field in ("url", "profile_url", "source_url", "import_source", "metrics_source", "provider", "network"):
|
|
2097
|
+
value = str(channel.get(field) or "").strip().casefold()
|
|
2098
|
+
if "tiktok.com" in value or value.startswith("tiktok") or value == "tik-tok":
|
|
2099
|
+
return False
|
|
2100
|
+
for field in ("tiktok_id", "tiktok_username", "tiktok_handle", "tiktok_user_id", "tiktok_open_id"):
|
|
2101
|
+
if str(channel.get(field) or "").strip():
|
|
2102
|
+
return False
|
|
2103
|
+
return True
|
|
2104
|
+
|
|
2105
|
+
|
|
2089
2106
|
def render_channels():
|
|
2090
2107
|
st.title("Canais Youtube")
|
|
2091
2108
|
st.caption("Escolha entre importar dados públicos do YouTube ou preencher o canal manualmente.")
|
|
@@ -2342,10 +2359,7 @@ def render_channels():
|
|
|
2342
2359
|
|
|
2343
2360
|
st.divider()
|
|
2344
2361
|
st.subheader("Canais cadastrados")
|
|
2345
|
-
registered_channels = [
|
|
2346
|
-
channel for channel in read_json("channels.json", [])
|
|
2347
|
-
if isinstance(channel, dict) and str(channel.get("platform") or "youtube").strip().lower() != "tiktok"
|
|
2348
|
-
]
|
|
2362
|
+
registered_channels = [channel for channel in read_json("channels.json", []) if is_youtube_channel_record(channel)]
|
|
2349
2363
|
if not registered_channels:
|
|
2350
2364
|
st.info("Nenhum canal cadastrado.")
|
|
2351
2365
|
else:
|
|
@@ -7206,6 +7220,9 @@ def render_settings():
|
|
|
7206
7220
|
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.")
|
|
7207
7221
|
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")
|
|
7208
7222
|
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.")
|
|
7223
|
+
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.")
|
|
7224
|
+
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.")
|
|
7225
|
+
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`.")
|
|
7209
7226
|
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)
|
|
7210
7227
|
_render_credential_status(composio_api_key)
|
|
7211
7228
|
composio_action_cols = st.columns(2)
|
|
@@ -7214,7 +7231,7 @@ def render_settings():
|
|
|
7214
7231
|
with composio_action_cols[1]:
|
|
7215
7232
|
test_composio = st.button("Testar configuração", use_container_width=True, key="upload_composio_test")
|
|
7216
7233
|
if save_composio:
|
|
7217
|
-
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 "{}"})
|
|
7234
|
+
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 "{}"})
|
|
7218
7235
|
write_json("settings.json", settings)
|
|
7219
7236
|
st.success("Configuração Composio guardada.")
|
|
7220
7237
|
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