@danhachuel/thunderbolt 0.4.56 → 0.4.58

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.
@@ -825,3 +825,15 @@ Na interface, uma tarefa em execução mostra a última actividade do helper e o
825
825
  A partir da versão 0.3.78, o card **Backlog Vídeos** mostra a causa terminal e disponibiliza **Ver diagnóstico completo**. As mensagens `Pexels key validation completed`, `installing or verifying project dependencies with uv` e `TASK_DIR` são apenas progresso ou metadados; não são, por si só, uma falha.
826
826
 
827
827
  Se o `uv` falhar, o erro inclui o detalhe seguro do resolver. Se a CLI do MoneyPrinterTurbo falhar, o resumo inclui `MPT_ERROR` e mantém `Log completo` e `Manifesto`. A interface não exibe chaves, cookies ou tokens. O tempo limite da verificação de dependências é de 15 minutos; a etapa é encerrada em vez de ficar indefinidamente sem resposta.
828
+
829
+ ## 18. Upload via Composio
830
+
831
+ Em **Pipeline Vídeos > Upload > Upload via Composio**, o Thunderbolt permite descobrir ferramentas de upload disponíveis no projecto Composio Platform, seleccionar um toolkit e enviar um vídeo pronto da pipeline. Como cada provider expõe ferramentas e campos diferentes, o slug da ferramenta e o campo que recebe o vídeo são escolhidos explicitamente na interface; nenhum destino é assumido automaticamente.
832
+
833
+ Configure primeiro **Configurações > Configuração API > API Keys Upload > Composio**. Active a integração, introduza a API key de projecto Composio Platform e defina um `user ID` estável para associar as contas conectadas. A chave permanece no storage local e não é incluída no histórico, nos logs nem no repositório. O botão de teste faz apenas descoberta read-only.
834
+
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
+
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.
package/app/main.py CHANGED
@@ -98,6 +98,7 @@ from integrations.youtube_batch import account_key as youtube_batch_account_key,
98
98
  from integrations.local_runtime import MoneyPrinterRuntime
99
99
  from integrations.moneyprinter_config import sync_moneyprinter_config
100
100
  from integrations.openai_model_discovery import DEFAULT_NVIDIA_NIM_BASE_URL
101
+ from integrations.composio_upload import ComposioUploadError, authorize_toolkit, discover_tools, execute_upload, parse_arguments, test_configuration
101
102
 
102
103
  DEFAULT_UI_LANGUAGE = "en"
103
104
 
@@ -5302,7 +5303,7 @@ def render_music_upload() -> None:
5302
5303
 
5303
5304
  def render_upload():
5304
5305
  st.title("Upload")
5305
- upload_tab, direct_tab, postiz_tab, upload_post_tab = render_localized_tabs(["Upload convencional", "Upload directo", "Postiz", "Upload-Post"])
5306
+ upload_tab, direct_tab, postiz_tab, upload_post_tab, composio_tab = render_localized_tabs(["Upload convencional", "Upload directo", "Postiz", "Upload-Post", "Upload via Composio"])
5306
5307
  with direct_tab:
5307
5308
  render_upload_direct()
5308
5309
  with postiz_tab:
@@ -5311,6 +5312,81 @@ def render_upload():
5311
5312
  render_upload_post()
5312
5313
  with upload_tab:
5313
5314
  render_upload_conventional()
5315
+ with composio_tab:
5316
+ render_upload_composio()
5317
+
5318
+
5319
+ def render_upload_composio():
5320
+ st.subheader("Upload via Composio")
5321
+ st.caption("Descubra uma ferramenta de upload Composio, ligue a conta do provider quando necessário e envie um vídeo seleccionado. O slug e o campo do ficheiro são sempre escolhidos explicitamente.")
5322
+ settings = read_json("settings.json", {})
5323
+ api_key = str(settings.get("composio_api_key") or "").strip()
5324
+ user_id = str(settings.get("composio_user_id") or "").strip()
5325
+ if not settings.get("composio_enabled", False) or not api_key:
5326
+ st.warning("Composio está desactivado ou sem API key. Configure-o em Configuração API > API Keys Upload > Composio.")
5327
+ return
5328
+ query = st.text_input("Pesquisar ferramenta Composio", value="upload a video file", key="composio_upload_query")
5329
+ toolkit_filter = st.text_input("Toolkit preferido (opcional)", value=str(settings.get("composio_toolkit") or ""), key="composio_upload_toolkit")
5330
+ if st.button("Descobrir ferramentas", key="composio_upload_discover", use_container_width=True):
5331
+ try:
5332
+ st.session_state["composio_upload_tools"] = discover_tools(api_key, user_id, query, toolkit_filter)
5333
+ if not st.session_state["composio_upload_tools"]:
5334
+ st.info("Nenhuma ferramenta encontrada. Tente uma pesquisa mais específica ou deixe o toolkit vazio.")
5335
+ else:
5336
+ st.success(f"{len(st.session_state['composio_upload_tools'])} ferramenta(s) encontrada(s).")
5337
+ except ComposioUploadError as exc:
5338
+ st.error(str(exc))
5339
+ tools = [item for item in st.session_state.get("composio_upload_tools", []) if isinstance(item, dict) and item.get("slug")]
5340
+ if not tools:
5341
+ st.info("Clique em Descobrir ferramentas para carregar os destinos disponíveis na sua conta Composio.")
5342
+ return
5343
+ tool_slugs = [str(item["slug"]) for item in tools]
5344
+ selected_slug = st.selectbox("Ferramenta de upload", tool_slugs, format_func=lambda slug: next((f"{item.get('name') or slug} — {item.get('toolkit') or 'toolkit desconhecido'}" for item in tools if item.get("slug") == slug), slug), key="composio_upload_slug")
5345
+ selected_tool = next((item for item in tools if item.get("slug") == selected_slug), tools[0])
5346
+ if selected_tool.get("description"):
5347
+ st.caption(str(selected_tool["description"]))
5348
+ schema = selected_tool.get("schema") or {}
5349
+ with st.expander("Schema da ferramenta", expanded=False):
5350
+ st.json(schema)
5351
+ file_field = st.text_input("Campo que recebe o vídeo", value=str(st.session_state.get("composio_upload_file_field") or "file"), key="composio_upload_file_field")
5352
+ arguments_json = st.text_area("Argumentos JSON adicionais", value=str(st.session_state.get("composio_upload_arguments") or "{}"), height=150, key="composio_upload_arguments")
5353
+ if st.button("Autorizar toolkit no Composio", key="composio_upload_authorize", use_container_width=True):
5354
+ try:
5355
+ auth = authorize_toolkit(api_key, user_id, str(selected_tool.get("toolkit") or ""))
5356
+ if auth.get("redirect_url"):
5357
+ st.success("Connect Link criado. Abra o link para autorizar a conta e depois repita o envio.")
5358
+ st.markdown(f"[Abrir Connect Link do Composio]({auth['redirect_url']})")
5359
+ else:
5360
+ st.info("O Composio não devolveu um Connect Link; confirme o estado da conta no dashboard.")
5361
+ except ComposioUploadError as exc:
5362
+ st.error(str(exc))
5363
+ tasks = [task for task in read_json("tasks.json", []) if task.get("state") == "done" or task.get("artifacts", {}).get("video")]
5364
+ if not tasks:
5365
+ st.info("Não há vídeos prontos para enviar via Composio.")
5366
+ return
5367
+ task_options = {str(task.get("id")): task for task in tasks if task.get("id")}
5368
+ selected_task_id = st.selectbox("Vídeo", list(task_options), format_func=lambda task_id: f"{task_options[task_id].get('topic') or 'Vídeo Thunderbolt'} — {task_options[task_id].get('artifacts', {}).get('video') or 'sem ficheiro'}", key="composio_upload_task")
5369
+ task = task_options[selected_task_id]
5370
+ video_path = str((task.get("artifacts", {}) or {}).get("video") or "")
5371
+ st.caption(video_path or "Sem caminho de vídeo registado")
5372
+ st.info(f"Resumo: `{selected_slug}` · toolkit `{selected_tool.get('toolkit') or 'não indicado'}` · campo `{file_field or 'não indicado'}`")
5373
+ if st.button("Enviar vídeo via Composio", type="primary", key=f"composio_upload_send_{selected_task_id}", use_container_width=True):
5374
+ try:
5375
+ parse_arguments(arguments_json)
5376
+ result = execute_upload(api_key, user_id, selected_slug, video_path, file_field, arguments_json)
5377
+ record = {"id": uuid.uuid4().hex, "task_id": task.get("id"), "destination": "Composio", "target": {"toolkit": selected_tool.get("toolkit"), "slug": selected_slug, "file_field": file_field}, "status": "published" if result.get("successful") else "failed", "message": result.get("error") or "Upload Composio concluído.", "data": result.get("data") or {}, "log_id": result.get("log_id") or "", "created_at": now()}
5378
+ uploads = read_json("uploads.json", [])
5379
+ uploads.append(record)
5380
+ write_json("uploads.json", uploads)
5381
+ reconcile_persisted_notifications()
5382
+ if result.get("successful"):
5383
+ st.success(record["message"])
5384
+ else:
5385
+ st.error(record["message"])
5386
+ if result.get("log_id"):
5387
+ st.caption(f"Composio log ID: {result['log_id']}")
5388
+ except ComposioUploadError as exc:
5389
+ st.error(str(exc))
5314
5390
 
5315
5391
 
5316
5392
  def render_upload_postiz():
@@ -7089,7 +7165,33 @@ def render_settings():
7089
7165
  with st.expander("API Bilibili", expanded=False):
7090
7166
  render_bilibili_api_cards(settings)
7091
7167
  with st.expander("Composio", expanded=False):
7092
- st.info("Secção reservada para uma futura integração Composio.")
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
+ 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.")
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.")
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.")
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_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)
7177
+ _render_credential_status(composio_api_key)
7178
+ composio_action_cols = st.columns(2)
7179
+ with composio_action_cols[0]:
7180
+ save_composio = st.button("Guardar Composio", type="primary", use_container_width=True, key="upload_composio_save")
7181
+ with composio_action_cols[1]:
7182
+ test_composio = st.button("Testar configuração", use_container_width=True, key="upload_composio_test")
7183
+ if save_composio:
7184
+ 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_arguments_json": composio_arguments_json.strip() or "{}"})
7185
+ write_json("settings.json", settings)
7186
+ st.success("Configuração Composio guardada.")
7187
+ st.rerun()
7188
+ if test_composio:
7189
+ try:
7190
+ result = test_configuration(composio_api_key, composio_user_id)
7191
+ found = len((result.get("data") or {}).get("tools", []))
7192
+ st.success(f"Configuração Composio válida; {found} ferramenta(s) de upload encontradas.")
7193
+ except ComposioUploadError as exc:
7194
+ st.error(str(exc))
7093
7195
  with st.expander("Upload-Post", expanded=False):
7094
7196
  upload_post_enabled_upload = st.checkbox("Activar Upload-Post", bool(settings.get("upload_post_enabled", False)), key="upload_tab_upload_post_enabled")
7095
7197
  upload_post_key_upload = st.text_input("Upload-Post API key", value=str(settings.get("upload_post_api_key") or ""), type="password", key="upload_tab_upload_post_key")
@@ -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,14 @@ 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_arguments_json": "{}",
290
298
  "postiz_enabled": False,
291
299
  "postiz_api_key": "",
292
300
  "postiz_base_url": "https://api.postiz.com/public/v1",
@@ -0,0 +1,174 @@
1
+ """Composio Platform adapter for user-selected video upload tools."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ class ComposioUploadError(ValueError):
10
+ """Safe, user-facing validation or integration error."""
11
+
12
+
13
+ def _safe_value(value: Any) -> Any:
14
+ if value is None or isinstance(value, (str, int, float, bool)):
15
+ return value
16
+ if isinstance(value, dict):
17
+ return {str(key): _safe_value(item) for key, item in value.items()}
18
+ if isinstance(value, (list, tuple)):
19
+ return [_safe_value(item) for item in value]
20
+ for method in ("model_dump", "to_dict", "dict"):
21
+ converter = getattr(value, method, None)
22
+ if callable(converter):
23
+ try:
24
+ return _safe_value(converter())
25
+ except Exception:
26
+ pass
27
+ attributes = getattr(value, "__dict__", None)
28
+ if isinstance(attributes, dict):
29
+ return {str(key): _safe_value(item) for key, item in attributes.items()}
30
+ return str(value)
31
+
32
+
33
+ def _tool_item(tool: Any) -> dict[str, Any]:
34
+ raw = _safe_value(tool)
35
+ if isinstance(raw, dict) and isinstance(raw.get("function"), dict):
36
+ function = raw["function"]
37
+ return {
38
+ "slug": str(function.get("name") or raw.get("name") or ""),
39
+ "name": str(function.get("name") or raw.get("name") or ""),
40
+ "description": str(function.get("description") or raw.get("description") or ""),
41
+ "toolkit": str(raw.get("toolkit") or ""),
42
+ "schema": function.get("parameters") or {},
43
+ }
44
+ if isinstance(raw, dict):
45
+ toolkit = raw.get("toolkit")
46
+ if isinstance(toolkit, dict):
47
+ toolkit = toolkit.get("slug") or toolkit.get("name") or ""
48
+ return {
49
+ "slug": str(raw.get("slug") or raw.get("name") or ""),
50
+ "name": str(raw.get("name") or raw.get("slug") or ""),
51
+ "description": str(raw.get("description") or ""),
52
+ "toolkit": str(toolkit or ""),
53
+ "schema": raw.get("input_parameters") or raw.get("parameters") or {},
54
+ }
55
+ return {"slug": "", "name": "", "description": "", "toolkit": "", "schema": {}}
56
+
57
+
58
+ def _response(result: Any) -> dict[str, Any]:
59
+ raw = _safe_value(result)
60
+ if not isinstance(raw, dict):
61
+ raw = {"data": raw}
62
+ error = raw.get("error")
63
+ return {
64
+ "successful": not bool(error),
65
+ "data": raw.get("data") or {},
66
+ "error": str(error) if error else "",
67
+ "log_id": str(raw.get("log_id") or raw.get("request_id") or ""),
68
+ }
69
+
70
+
71
+ def _require_api_key(api_key: str) -> str:
72
+ value = str(api_key or "").strip()
73
+ if len(value) < 10 or value.lower() in {"placeholder", "your_composio_api_key"}:
74
+ raise ComposioUploadError("Configure uma API key válida do Composio em Configuração API > API Keys Upload > Composio.")
75
+ return value
76
+
77
+
78
+ def _require_user_id(user_id: str) -> str:
79
+ value = str(user_id or "").strip()
80
+ if not value:
81
+ raise ComposioUploadError("Indique um Composio user ID estável antes de continuar.")
82
+ return value
83
+
84
+
85
+ def _client(api_key: str, *, upload_dir: Path | None = None):
86
+ try:
87
+ from composio import Composio
88
+ except ImportError as exc:
89
+ raise ComposioUploadError("A SDK Python composio não está instalada. Execute a instalação do Thunderbolt novamente.") from exc
90
+ kwargs: dict[str, Any] = {"api_key": _require_api_key(api_key), "allow_tracking": False}
91
+ if upload_dir is not None:
92
+ kwargs.update({
93
+ "dangerously_allow_auto_upload_download_files": True,
94
+ "file_upload_dirs": [str(upload_dir.resolve())],
95
+ })
96
+ return Composio(**kwargs)
97
+
98
+
99
+ def discover_tools(api_key: str, user_id: str, query: str, toolkit: str = "") -> list[dict[str, Any]]:
100
+ client = _client(api_key)
101
+ try:
102
+ tools = client.tools.get(
103
+ _require_user_id(user_id),
104
+ search=(query or "upload a video file").strip(),
105
+ toolkits=[toolkit.strip()] if toolkit.strip() else None,
106
+ limit=10,
107
+ )
108
+ return [item for item in (_tool_item(tool) for tool in tools) if item.get("slug")]
109
+ except ComposioUploadError:
110
+ raise
111
+ except Exception as exc:
112
+ raise ComposioUploadError(f"Não foi possível descobrir ferramentas Composio: {type(exc).__name__}: {exc}") from exc
113
+
114
+
115
+ def authorize_toolkit(api_key: str, user_id: str, toolkit: str) -> dict[str, Any]:
116
+ toolkit = str(toolkit or "").strip()
117
+ if not toolkit:
118
+ raise ComposioUploadError("Seleccione uma ferramenta descoberta para saber qual toolkit deve ser autorizado.")
119
+ client = _client(api_key)
120
+ try:
121
+ request = client.create(user_id=_require_user_id(user_id)).authorize(toolkit)
122
+ return {"connected_account_id": str(getattr(request, "id", "") or ""), "redirect_url": str(getattr(request, "redirect_url", "") or "")}
123
+ except ComposioUploadError:
124
+ raise
125
+ except Exception as exc:
126
+ raise ComposioUploadError(f"Não foi possível criar o Connect Link do Composio: {type(exc).__name__}: {exc}") from exc
127
+
128
+
129
+ def parse_arguments(arguments_json: str) -> dict[str, Any]:
130
+ try:
131
+ parsed = json.loads(arguments_json or "{}")
132
+ except json.JSONDecodeError as exc:
133
+ raise ComposioUploadError(f"Os argumentos da ferramenta não são JSON válido: {exc.msg}.") from exc
134
+ if not isinstance(parsed, dict):
135
+ raise ComposioUploadError("Os argumentos da ferramenta devem ser um objecto JSON.")
136
+ return parsed
137
+
138
+
139
+ def execute_upload(api_key: str, user_id: str, slug: str, video_path: str, file_field: str, arguments_json: str = "") -> dict[str, Any]:
140
+ path = Path(str(video_path or "").strip()).expanduser()
141
+ slug = str(slug or "").strip()
142
+ file_field = str(file_field or "").strip()
143
+ if not path.is_file():
144
+ raise ComposioUploadError("O ficheiro de vídeo seleccionado não existe ou não é um ficheiro.")
145
+ if path.stat().st_size <= 0:
146
+ raise ComposioUploadError("O ficheiro de vídeo seleccionado está vazio.")
147
+ if not slug:
148
+ raise ComposioUploadError("Seleccione ou indique o slug de uma ferramenta Composio.")
149
+ if not file_field:
150
+ raise ComposioUploadError("Indique o campo de argumentos que recebe o ficheiro.")
151
+ arguments = parse_arguments(arguments_json)
152
+ if file_field in arguments and arguments[file_field] not in (None, "", str(path)):
153
+ raise ComposioUploadError(f"O campo `{file_field}` já contém um valor. Remova-o antes de injectar o vídeo.")
154
+ arguments[file_field] = str(path.resolve())
155
+ client = _client(api_key, upload_dir=path.parent)
156
+ try:
157
+ result = client.tools.execute(
158
+ slug,
159
+ arguments=arguments,
160
+ user_id=_require_user_id(user_id),
161
+ version="latest",
162
+ dangerously_skip_version_check=True,
163
+ )
164
+ return _response(result)
165
+ except ComposioUploadError:
166
+ raise
167
+ except Exception as exc:
168
+ safe_message = str(exc).replace(str(api_key), "[REDACTED]")
169
+ raise ComposioUploadError(f"A ferramenta Composio falhou: {type(exc).__name__}: {safe_message}") from exc
170
+
171
+
172
+ def test_configuration(api_key: str, user_id: str) -> dict[str, Any]:
173
+ tools = discover_tools(api_key, user_id, "upload a video file")
174
+ return {"successful": True, "data": {"tools": tools}, "error": "", "log_id": ""}
@@ -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,32 @@ 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
+ video_path=video_path,
110
+ title=title,
111
+ description=description,
112
+ tags=tags or [],
113
+ thumbnail_path=thumbnail_path,
114
+ captions_path=captions_path,
115
+ )
116
+ except Exception as exc:
117
+ composio_result = IntegrationResult(False, f"Composio falhou: {type(exc).__name__}: {exc}", {})
118
+ attempts.append(_attempt_record("Composio", composio_result))
119
+ if composio_result.ok:
120
+ return _result_with_attempts(composio_result, attempts, "Composio")
121
+
96
122
  quota_count = official_upload_count(channel, account)
97
123
  if quota_count >= OFFICIAL_DAILY_LIMIT:
98
124
  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 +210,22 @@ def upload_with_default_route(
184
210
  return _result_with_attempts(postiz_result, attempts, "Postiz")
185
211
 
186
212
  return IntegrationResult(False, "Nenhum método de envio conseguiu publicar o vídeo.", {"attempts": attempts, "route": "none"})
213
+
214
+
215
+ def _composio_upload(settings: dict[str, Any], **kwargs: Any) -> IntegrationResult:
216
+ try:
217
+ result = execute_upload(
218
+ str(settings.get("composio_api_key") or ""),
219
+ str(settings.get("composio_user_id") or ""),
220
+ str(settings.get("composio_tool_slug") or ""),
221
+ str(kwargs.get("video_path") or ""),
222
+ str(settings.get("composio_file_field") or "file"),
223
+ str(settings.get("composio_arguments_json") or "{}"),
224
+ )
225
+ except ComposioUploadError as exc:
226
+ return IntegrationResult(False, str(exc), {})
227
+ return IntegrationResult(
228
+ bool(result.get("successful")),
229
+ str(result.get("error") or "Upload via Composio concluído."),
230
+ {"composio_data": result.get("data") or {}, "log_id": result.get("log_id") or ""},
231
+ )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.4.56",
3
+ "version": "0.4.58",
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",
package/requirements.txt CHANGED
@@ -22,3 +22,4 @@ kagglehub>=0.3,<1
22
22
  supabase>=2.4,<3
23
23
  bilibili-api-python>=17.4,<18
24
24
  playwright>=1.45,<2
25
+ composio>=0.21,<1
package/scripts/cli.mjs CHANGED
@@ -116,7 +116,7 @@ function check() {
116
116
  console.error(`Python não encontrado. Execute: npx.cmd --yes @danhachuel/thunderbolt install`);
117
117
  process.exit(1);
118
118
  }
119
- const requiredModules = ["streamlit", "requests", "pandas", "toml", "imageio_ffmpeg", "edge_tts", "sklearn", "mlxtend", "plotly", "seaborn", "kagglehub"];
119
+ const requiredModules = ["streamlit", "requests", "pandas", "toml", "imageio_ffmpeg", "edge_tts", "sklearn", "mlxtend", "plotly", "seaborn", "kagglehub", "composio"];
120
120
  const missing = requiredModules.filter((moduleName) => !moduleAvailable(moduleName));
121
121
  const ffmpeg = moduleAvailable("imageio_ffmpeg");
122
122
  const mptPath = configuredMoneyPrinterPath();