@danhachuel/thunderbolt 0.4.57 → 0.4.59
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
CHANGED
|
@@ -835,3 +835,7 @@ Configure primeiro **Configurações > Configuração API > API Keys Upload > Co
|
|
|
835
835
|
Quando o provider ainda não estiver conectado, use **Autorizar toolkit no Composio**. O Thunderbolt apresenta o Connect Link devolvido pelo Composio; conclua a autorização nesse link e repita o envio. O Thunderbolt não implementa nem armazena um fluxo OAuth próprio.
|
|
836
836
|
|
|
837
837
|
Antes de clicar em **Enviar vídeo via Composio**, confirme o vídeo, o toolkit, o slug da ferramenta, o campo do ficheiro e os argumentos JSON. O resultado guarda apenas um registo local mínimo com o estado e, quando devolvido, o `log_id` do Composio. URLs presigned, tokens, headers e a API key não são guardados no histórico.
|
|
838
|
+
|
|
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
|
+
|
|
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.
|
package/app/main.py
CHANGED
|
@@ -7167,9 +7167,14 @@ def render_settings():
|
|
|
7167
7167
|
with st.expander("Composio", expanded=False):
|
|
7168
7168
|
st.caption("A API key é guardada apenas no storage local. O slug da ferramenta e o provider são descobertos no Upload via Composio; a autenticação da conta é feita pelo Connect Link do Composio.")
|
|
7169
7169
|
composio_enabled = st.checkbox("Activar Composio", value=bool(settings.get("composio_enabled", False)), key="upload_composio_enabled")
|
|
7170
|
+
composio_auto_upload = st.checkbox("Usar Composio por defeito na Automação Youtube", value=bool(settings.get("composio_auto_upload", True)), key="upload_composio_auto_upload", help="Quando estiver configurado, Composio é tentado antes da API Oficial, Upload directo e Postiz.")
|
|
7170
7171
|
composio_api_key = st.text_input("Composio API key", value=str(settings.get("composio_api_key") or ""), type="password", key="upload_composio_api_key", help="Use a API key de projecto do Composio Platform. Nunca coloque esta chave no GitHub, em URLs ou em mensagens.")
|
|
7171
7172
|
composio_user_id = st.text_input("Composio user ID", value=str(settings.get("composio_user_id") or "thunderbolt-local"), key="upload_composio_user_id", help="Identidade estável usada para associar as contas conectadas no Composio.")
|
|
7172
7173
|
composio_toolkit = st.text_input("Toolkit preferido (opcional)", value=str(settings.get("composio_toolkit") or ""), key="upload_composio_toolkit", help="Deixe vazio para descobrir ferramentas em todos os toolkits.")
|
|
7174
|
+
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.")
|
|
7175
|
+
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")
|
|
7176
|
+
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.")
|
|
7177
|
+
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)
|
|
7173
7178
|
_render_credential_status(composio_api_key)
|
|
7174
7179
|
composio_action_cols = st.columns(2)
|
|
7175
7180
|
with composio_action_cols[0]:
|
|
@@ -7177,7 +7182,7 @@ def render_settings():
|
|
|
7177
7182
|
with composio_action_cols[1]:
|
|
7178
7183
|
test_composio = st.button("Testar configuração", use_container_width=True, key="upload_composio_test")
|
|
7179
7184
|
if save_composio:
|
|
7180
|
-
settings.update({"composio_enabled": bool(composio_enabled), "composio_api_key": composio_api_key.strip(), "composio_user_id": composio_user_id.strip() or "thunderbolt-local", "composio_toolkit": composio_toolkit.strip()})
|
|
7185
|
+
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 "{}"})
|
|
7181
7186
|
write_json("settings.json", settings)
|
|
7182
7187
|
st.success("Configuração Composio guardada.")
|
|
7183
7188
|
st.rerun()
|
|
@@ -1474,9 +1474,10 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
|
|
|
1474
1474
|
return _update(task_id, stage="upload", state="done", progress=100, artifacts=artifacts, video_ready=True, error=None)
|
|
1475
1475
|
|
|
1476
1476
|
configured_account_id = str(channel.get("google_account_id") or "").strip()
|
|
1477
|
+
configured_composio = bool(settings.get("composio_enabled", False)) and bool(settings.get("composio_auto_upload", True)) and bool(settings.get("composio_api_key")) and bool(settings.get("composio_tool_slug"))
|
|
1477
1478
|
configured_upload_post = bool(settings.get("upload_post_enabled", False)) and bool(settings.get("upload_post_auto_upload", False))
|
|
1478
1479
|
configured_postiz = bool(settings.get("postiz_enabled", False)) and bool(settings.get("postiz_auto_publish", False))
|
|
1479
|
-
if not (configured_account_id or configured_upload_post or configured_postiz):
|
|
1480
|
+
if not (configured_composio or configured_account_id or configured_upload_post or configured_postiz):
|
|
1480
1481
|
artifacts["upload"] = {
|
|
1481
1482
|
"route": "local",
|
|
1482
1483
|
"status": "skipped",
|
package/hermes_ui/storage.py
CHANGED
|
@@ -287,6 +287,15 @@ DEFAULTS: dict[str, Any] = {
|
|
|
287
287
|
"upload_post_username": "",
|
|
288
288
|
"upload_post_platforms": "tiktok,instagram",
|
|
289
289
|
"upload_post_auto_upload": False,
|
|
290
|
+
"composio_enabled": True,
|
|
291
|
+
"composio_auto_upload": True,
|
|
292
|
+
"composio_api_key": "",
|
|
293
|
+
"composio_user_id": "thunderbolt-local",
|
|
294
|
+
"composio_toolkit": "",
|
|
295
|
+
"composio_tool_slug": "",
|
|
296
|
+
"composio_file_field": "file",
|
|
297
|
+
"composio_channel_field": "channel_id",
|
|
298
|
+
"composio_arguments_json": "{}",
|
|
290
299
|
"postiz_enabled": False,
|
|
291
300
|
"postiz_api_key": "",
|
|
292
301
|
"postiz_base_url": "https://api.postiz.com/public/v1",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Ordered YouTube upload routing for Thunderbolt.
|
|
2
2
|
|
|
3
3
|
The route is intentionally deterministic:
|
|
4
|
-
1.
|
|
5
|
-
2.
|
|
6
|
-
3.
|
|
4
|
+
1. Composio, when the configured upload tool is available.
|
|
5
|
+
2. Official YouTube API, up to five successful sends per Google account/day.
|
|
6
|
+
3. Internal browser-session upload, when the account document is complete.
|
|
7
|
+
4. Postiz, when configured with an API key and integration ID.
|
|
7
8
|
"""
|
|
8
9
|
|
|
9
10
|
from __future__ import annotations
|
|
@@ -17,6 +18,7 @@ from integrations.postiz import PostizAdapter
|
|
|
17
18
|
from integrations.session_info_health import check_account_session_info_health, emit_session_info_health_alerts
|
|
18
19
|
from integrations.youtube_direct_credentials import document_status
|
|
19
20
|
from integrations.youtube_direct_upload import YouTubeDirectUploader
|
|
21
|
+
from integrations.composio_upload import ComposioUploadError, execute_upload
|
|
20
22
|
from hermes_ui.storage import read_json, write_json
|
|
21
23
|
|
|
22
24
|
OFFICIAL_DAILY_LIMIT = 5
|
|
@@ -91,8 +93,33 @@ def upload_with_default_route(
|
|
|
91
93
|
official_uploader: Callable[..., IntegrationResult] | None = None,
|
|
92
94
|
direct_uploader: Callable[..., IntegrationResult] | None = None,
|
|
93
95
|
postiz_publisher: Callable[..., IntegrationResult] | None = None,
|
|
96
|
+
composio_publisher: Callable[..., IntegrationResult] | None = None,
|
|
94
97
|
) -> IntegrationResult:
|
|
95
98
|
attempts: list[dict[str, Any]] = []
|
|
99
|
+
composio_enabled = bool(settings.get("composio_enabled", False)) and bool(settings.get("composio_auto_upload", True))
|
|
100
|
+
composio_ready = composio_enabled and bool(settings.get("composio_api_key")) and bool(settings.get("composio_tool_slug"))
|
|
101
|
+
if composio_enabled and not composio_ready:
|
|
102
|
+
composio_result = IntegrationResult(False, "Composio está activo, mas falta API key ou slug da ferramenta de upload.", {})
|
|
103
|
+
attempts.append(_attempt_record("Composio", composio_result, skipped=True))
|
|
104
|
+
elif composio_ready:
|
|
105
|
+
composio_publisher = composio_publisher or _composio_upload
|
|
106
|
+
try:
|
|
107
|
+
composio_result = composio_publisher(
|
|
108
|
+
settings,
|
|
109
|
+
channel=channel,
|
|
110
|
+
video_path=video_path,
|
|
111
|
+
title=title,
|
|
112
|
+
description=description,
|
|
113
|
+
tags=tags or [],
|
|
114
|
+
thumbnail_path=thumbnail_path,
|
|
115
|
+
captions_path=captions_path,
|
|
116
|
+
)
|
|
117
|
+
except Exception as exc:
|
|
118
|
+
composio_result = IntegrationResult(False, f"Composio falhou: {type(exc).__name__}: {exc}", {})
|
|
119
|
+
attempts.append(_attempt_record("Composio", composio_result))
|
|
120
|
+
if composio_result.ok:
|
|
121
|
+
return _result_with_attempts(composio_result, attempts, "Composio")
|
|
122
|
+
|
|
96
123
|
quota_count = official_upload_count(channel, account)
|
|
97
124
|
if quota_count >= OFFICIAL_DAILY_LIMIT:
|
|
98
125
|
quota_result = IntegrationResult(False, f"Limite local de {OFFICIAL_DAILY_LIMIT} envios oficiais por conta Google atingido hoje; a tentar o próximo método.", {"count": quota_count, "limit": OFFICIAL_DAILY_LIMIT})
|
|
@@ -184,3 +211,37 @@ def upload_with_default_route(
|
|
|
184
211
|
return _result_with_attempts(postiz_result, attempts, "Postiz")
|
|
185
212
|
|
|
186
213
|
return IntegrationResult(False, "Nenhum método de envio conseguiu publicar o vídeo.", {"attempts": attempts, "route": "none"})
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _composio_upload(settings: dict[str, Any], *, channel: dict[str, Any], **kwargs: Any) -> IntegrationResult:
|
|
217
|
+
channel_id = str(channel.get("youtube_channel_id") or "").strip()
|
|
218
|
+
if not channel_id:
|
|
219
|
+
return IntegrationResult(False, "Composio não foi executado: o canal da tarefa não tem um YouTube channel ID sincronizado.", {"missing": ["youtube_channel_id"]})
|
|
220
|
+
channel_field = str(settings.get("composio_channel_field") or "channel_id").strip()
|
|
221
|
+
if not channel_field:
|
|
222
|
+
return IntegrationResult(False, "Composio não foi executado: configure o campo de canal da ferramenta.", {"missing": ["composio_channel_field"]})
|
|
223
|
+
try:
|
|
224
|
+
arguments = str(settings.get("composio_arguments_json") or "{}").strip() or "{}"
|
|
225
|
+
import json
|
|
226
|
+
parsed_arguments = json.loads(arguments)
|
|
227
|
+
if not isinstance(parsed_arguments, dict):
|
|
228
|
+
return IntegrationResult(False, "Composio não foi executado: os argumentos JSON devem ser um objecto.", {})
|
|
229
|
+
configured_channel = parsed_arguments.get(channel_field)
|
|
230
|
+
if configured_channel not in (None, "", channel_id):
|
|
231
|
+
return IntegrationResult(False, f"Composio bloqueado: o campo `{channel_field}` aponta para outro canal.", {"expected_channel_id": channel_id})
|
|
232
|
+
parsed_arguments[channel_field] = channel_id
|
|
233
|
+
result = execute_upload(
|
|
234
|
+
str(settings.get("composio_api_key") or ""),
|
|
235
|
+
str(settings.get("composio_user_id") or ""),
|
|
236
|
+
str(settings.get("composio_tool_slug") or ""),
|
|
237
|
+
str(kwargs.get("video_path") or ""),
|
|
238
|
+
str(settings.get("composio_file_field") or "file"),
|
|
239
|
+
json.dumps(parsed_arguments, ensure_ascii=False),
|
|
240
|
+
)
|
|
241
|
+
except ComposioUploadError as exc:
|
|
242
|
+
return IntegrationResult(False, str(exc), {})
|
|
243
|
+
return IntegrationResult(
|
|
244
|
+
bool(result.get("successful")),
|
|
245
|
+
str(result.get("error") or "Upload via Composio concluído."),
|
|
246
|
+
{"composio_data": result.get("data") or {}, "log_id": result.get("log_id") or ""},
|
|
247
|
+
)
|
package/package.json
CHANGED