@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)
|
package/hermes_ui/domain.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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(
|
|
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