@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.
@@ -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",
@@ -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. Official YouTube API, up to five successful sends per Google account/day.
5
- 2. Internal browser-session upload, when the account document is complete.
6
- 3. Postiz, when configured with an API key and integration ID.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.4.57",
3
+ "version": "0.4.59",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",