@danhachuel/thunderbolt 0.6.86 → 0.6.88

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/app/main.py CHANGED
@@ -77,7 +77,7 @@ def display_version(version: str) -> str:
77
77
 
78
78
  APP_VERSION_LABEL = display_version(APP_VERSION)
79
79
 
80
- from hermes_ui.domain import STAGES, create_batch, create_channel, create_tasks_for_batch, delete_channel, delete_task, pipeline_summary, remake_video_task, retry_task_with_current_settings, set_channel_defaults, stop_task_by_user, transition_task, update_channel, update_channel_video
80
+ from hermes_ui.domain import STAGES, composio_connected_account_id_from_channel_name, create_batch, create_channel, create_tasks_for_batch, delete_channel, delete_task, pipeline_summary, remake_video_task, retry_task_with_current_settings, set_channel_defaults, stop_task_by_user, transition_task, update_channel, update_channel_video
81
81
  from hermes_ui.channel_import import build_channel_template_xlsx, channel_is_duplicate, find_duplicate_channel, parse_channel_workbook, resolve_blueprint, resolve_google_account, resolve_voice
82
82
  from hermes_ui.drafts import list_drafts, save_draft
83
83
  from hermes_ui.automation_worker import load_worker_status
@@ -1399,6 +1399,7 @@ def render_channel_audio_subtitle_defaults(channel: dict) -> None:
1399
1399
  def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], youtube_account_labels: dict[str, str], youtube_accounts_by_id: dict[str, dict[str, Any]]) -> None:
1400
1400
  channel_id = str(channel["id"])
1401
1401
  blueprint_ids, blueprint_labels, current_blueprint, voice_options, current_voice = channel_default_options(channel)
1402
+ current_composio_account_id = str(channel.get("composio_connected_account_id") or composio_connected_account_id_from_channel_name(channel.get("name") or ""))
1402
1403
  account_ids = list(youtube_account_ids)
1403
1404
  current_account = str(channel.get("google_account_id") or "")
1404
1405
  if current_account and current_account not in account_ids:
@@ -1423,6 +1424,7 @@ def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], yout
1423
1424
  edited_blueprint = st.selectbox("Blueprint Padrão", blueprint_ids, index=blueprint_ids.index(current_blueprint) if current_blueprint in blueprint_ids else 0, format_func=lambda item: blueprint_labels.get(item, item or "Sem Blueprint padrão"))
1424
1425
  edited_voice = st.selectbox("Narrador/Voz Padrão", voice_options, index=voice_options.index(current_voice) if current_voice in voice_options else 0, format_func=lambda item: item or "Sem voz padrão")
1425
1426
  edited_account = st.selectbox("Conta Google para Upload directo", account_ids, index=account_ids.index(current_account) if current_account in account_ids else 0, format_func=lambda item: youtube_account_labels.get(item, item or "Sem conta Google associada"))
1427
+ edited_composio_account_id = st.text_input("Connected account ID do Composio", value=current_composio_account_id, help="Gerado automaticamente a partir do nome do canal no formato Nome-Do-Canal. Pode editar este valor para coincidir com o alias ligado no Composio.")
1426
1428
  edited_description = st.text_area("Descrição", value=str(channel.get("description") or ""), height=100)
1427
1429
  edited_automation = st.toggle("Automação ON", value=bool(channel.get("automation_on", False)), key=f"edit_automation_{channel_id}")
1428
1430
  edited_time = st.text_input("Horário diário (HH:MM)", value=str(channel.get("automation_time") or "00:00"))
@@ -1444,6 +1446,7 @@ def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], yout
1444
1446
  "default_blueprint_id": edited_blueprint.strip(), "blueprint_id": edited_blueprint.strip(),
1445
1447
  "default_voice": edited_voice.strip(), "voice": edited_voice.strip(),
1446
1448
  "google_account_id": edited_account.strip(), "google_account_email": str(youtube_accounts_by_id.get(edited_account, {}).get("email", "")),
1449
+ "composio_connected_account_id": edited_composio_account_id.strip() or composio_connected_account_id_from_channel_name(edited_name),
1447
1450
  "description": edited_description.strip(), "automation_on": bool(edited_automation), "automation_time": edited_time.strip(),
1448
1451
  "subscriber_count": _channel_count_value(edited_subscribers) or None, "video_count": _channel_count_value(edited_videos) or None, "view_count": _channel_count_value(edited_views) or None,
1449
1452
  })
@@ -6363,6 +6366,7 @@ def render_upload_composio():
6363
6366
  settings = read_json("settings.json", {})
6364
6367
  api_key = str(settings.get("composio_api_key") or "").strip()
6365
6368
  user_id = str(settings.get("composio_user_id") or "").strip()
6369
+ connected_account_id = str(settings.get("composio_connected_account_id") or "").strip()
6366
6370
  if not settings.get("composio_enabled", False) or not api_key:
6367
6371
  st.warning("Composio está desactivado ou sem API key. Configure-o em Configuração API > API Keys Upload > Composio.")
6368
6372
  return
@@ -6414,7 +6418,7 @@ def render_upload_composio():
6414
6418
  if st.button("Enviar vídeo via Composio", type="primary", key=f"composio_upload_send_{selected_task_id}", use_container_width=True):
6415
6419
  try:
6416
6420
  parse_arguments(arguments_json)
6417
- result = execute_upload(api_key, user_id, selected_slug, video_path, file_field, arguments_json)
6421
+ result = execute_upload(api_key, user_id, selected_slug, video_path, file_field, arguments_json, connected_account_id)
6418
6422
  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()}
6419
6423
  uploads = read_json("uploads.json", [])
6420
6424
  uploads.append(record)
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
+ import unicodedata
4
5
  import uuid
5
6
  from typing import Any
6
7
 
@@ -21,6 +22,13 @@ def slugify(value: str) -> str:
21
22
  return value or "item"
22
23
 
23
24
 
25
+ def composio_connected_account_id_from_channel_name(name: str) -> str:
26
+ """Create the editable Composio account alias used by a YouTube channel."""
27
+ normalized = unicodedata.normalize("NFKD", str(name or "")).encode("ascii", "ignore").decode("ascii")
28
+ words = re.findall(r"[A-Za-z0-9]+", normalized)
29
+ return "-".join(word[:1].upper() + word[1:] for word in words)
30
+
31
+
24
32
  def make_id(prefix: str) -> str:
25
33
  return f"{prefix}_{uuid.uuid4().hex[:10]}"
26
34
 
@@ -31,6 +39,7 @@ def create_channel(name: str, url: str = "", metadata: dict[str, Any] | None = N
31
39
  "youtube_channel_id": "",
32
40
  "google_account_id": "",
33
41
  "google_account_email": "",
42
+ "composio_connected_account_id": composio_connected_account_id_from_channel_name(name),
34
43
  "name": name.strip(),
35
44
  "url": url.strip(),
36
45
  "platform": "youtube",
@@ -112,28 +112,34 @@ def _client(api_key: str, *, upload_dir: Path | None = None):
112
112
 
113
113
 
114
114
  def _connected_account_id(client: Any, user_id: str, toolkit: str, selector: str) -> str:
115
- """Resolve a connected-account ID from an ID or UI alias."""
115
+ """Resolve a connected-account ID from an ID, alias, or sole active account."""
116
116
  value = str(selector or "").strip()
117
- if not value:
118
- return ""
119
117
  try:
120
118
  response = client.connected_accounts.list(user_ids=[_require_user_id(user_id)], statuses=["ACTIVE"])
121
119
  raw = _safe_value(response)
122
120
  items = raw.get("items", []) if isinstance(raw, dict) else raw
123
121
  if not isinstance(items, list):
124
122
  items = []
125
- wanted = value.casefold()
123
+ matching_items = []
126
124
  for item in items:
127
125
  if not isinstance(item, dict):
128
126
  continue
129
127
  toolkit_value = item.get("toolkit")
130
128
  if isinstance(toolkit_value, dict):
131
129
  toolkit_value = toolkit_value.get("slug") or toolkit_value.get("name")
132
- candidates = [item.get("id"), item.get("nanoid"), item.get("alias"), item.get("name")]
133
130
  if str(toolkit or "").strip() and toolkit.casefold() not in str(toolkit_value or "").casefold():
134
131
  continue
135
- if any(str(candidate or "").strip().casefold() == wanted for candidate in candidates):
136
- return str(item.get("id") or item.get("nanoid") or value).strip()
132
+ matching_items.append(item)
133
+ if value:
134
+ wanted = value.casefold()
135
+ for item in matching_items:
136
+ candidates = [item.get("id"), item.get("nanoid"), item.get("alias"), item.get("name")]
137
+ if any(str(candidate or "").strip().casefold() == wanted for candidate in candidates):
138
+ return str(item.get("id") or item.get("nanoid") or value).strip()
139
+ return value
140
+ if len(matching_items) == 1:
141
+ item = matching_items[0]
142
+ return str(item.get("id") or item.get("nanoid") or "").strip()
137
143
  except Exception:
138
144
  # Preserve the original selector so Composio returns its actionable error.
139
145
  return value
@@ -237,8 +243,12 @@ def execute_upload(api_key: str, user_id: str, slug: str, video_path: str, file_
237
243
  "dangerously_skip_version_check": True,
238
244
  }
239
245
  selected_account = _connected_account_id(client, user_id, "youtube", connected_account_id)
240
- if selected_account:
241
- execute_kwargs["connected_account_id"] = selected_account
246
+ if not selected_account:
247
+ raise ComposioUploadError(
248
+ f"Nenhuma conta YouTube activa está ligada ao Composio user ID `{_require_user_id(user_id)}`. "
249
+ "Use `Autorizar toolkit no Composio` ou configure o Connected account ID correcto."
250
+ )
251
+ execute_kwargs["connected_account_id"] = selected_account
242
252
  result = client.tools.execute(slug, **execute_kwargs)
243
253
  response = _response(result)
244
254
  if not response["successful"] and not response["error"]:
@@ -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.youtube_session_manager import renew_account_session
22
22
  from integrations.composio_upload import ComposioUploadError, execute_upload, resolve_tool_slug
23
+ from hermes_ui.domain import composio_connected_account_id_from_channel_name
23
24
  from hermes_ui.storage import read_json, write_json
24
25
  from hermes_ui.languages import language_locale
25
26
 
@@ -338,7 +339,7 @@ def _composio_upload(settings: dict[str, Any], *, channel: dict[str, Any], **kwa
338
339
  str(kwargs.get("video_path") or ""),
339
340
  file_field,
340
341
  json.dumps(parsed_arguments, ensure_ascii=False),
341
- str(settings.get("composio_connected_account_id") or channel.get("composio_connected_account_id") or "").strip(),
342
+ str(channel.get("composio_connected_account_id") or composio_connected_account_id_from_channel_name(channel.get("name") or "") or settings.get("composio_connected_account_id") or "").strip(),
342
343
  )
343
344
  except ComposioUploadError as exc:
344
345
  return IntegrationResult(False, str(exc), {})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.86",
3
+ "version": "0.6.88",
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",