@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.
- package/MANUAL-INSTALACAO.md +12 -0
- package/app/main.py +104 -2
- package/hermes_ui/pipeline_worker.py +2 -1
- package/hermes_ui/storage.py +8 -0
- package/integrations/composio_upload.py +174 -0
- package/integrations/upload_routing.py +48 -3
- package/package.json +1 -1
- package/requirements.txt +1 -0
- package/scripts/cli.mjs +1 -1
package/MANUAL-INSTALACAO.md
CHANGED
|
@@ -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.
|
|
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",
|
package/hermes_ui/storage.py
CHANGED
|
@@ -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.
|
|
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,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
package/requirements.txt
CHANGED
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();
|