@danhachuel/thunderbolt 0.6.6 → 0.6.7
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.
|
@@ -10,6 +10,14 @@ class ComposioUploadError(ValueError):
|
|
|
10
10
|
"""Safe, user-facing validation or integration error."""
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
COMPOSIO_OPERATION_SEARCH = {
|
|
14
|
+
"upload_video": {"query": "Upload Video", "toolkit": "YOUTUBE"},
|
|
15
|
+
"update_video": {"query": "Update Video", "toolkit": "YOUTUBE"},
|
|
16
|
+
"upload_tiktok_video": {"query": "Upload Video", "toolkit": "TIKTOK"},
|
|
17
|
+
"upload_instagram_media": {"query": "Upload Video Reel Photo", "toolkit": "INSTAGRAM"},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
13
21
|
def _safe_value(value: Any) -> Any:
|
|
14
22
|
if value is None or isinstance(value, (str, int, float, bool)):
|
|
15
23
|
return value
|
|
@@ -119,6 +127,38 @@ def discover_tools(api_key: str, user_id: str, query: str, toolkit: str = "") ->
|
|
|
119
127
|
raise ComposioUploadError(f"Não foi possível descobrir ferramentas Composio: {type(exc).__name__}: {exc}") from exc
|
|
120
128
|
|
|
121
129
|
|
|
130
|
+
def resolve_tool_slug(api_key: str, user_id: str, configured_slug: str, toolkit: str = "") -> str:
|
|
131
|
+
"""Resolve a Thunderbolt operation alias to a real Composio tool slug."""
|
|
132
|
+
slug = str(configured_slug or "").strip()
|
|
133
|
+
operation = COMPOSIO_OPERATION_SEARCH.get(slug)
|
|
134
|
+
if operation is None:
|
|
135
|
+
return slug
|
|
136
|
+
search_toolkit = str(operation["toolkit"] or toolkit or "").strip()
|
|
137
|
+
tools = discover_tools(api_key, user_id, str(operation["query"]), search_toolkit)
|
|
138
|
+
if not tools:
|
|
139
|
+
raise ComposioUploadError(
|
|
140
|
+
f"Não foi encontrada uma ferramenta Composio para `{slug}`. "
|
|
141
|
+
f"Ligue o toolkit {search_toolkit or 'correspondente'} e use Descobrir ferramentas."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def score(item: dict[str, Any]) -> tuple[int, str]:
|
|
145
|
+
candidate = str(item.get("slug") or "").strip()
|
|
146
|
+
normalized = candidate.upper().replace("-", "_")
|
|
147
|
+
if slug == "upload_video":
|
|
148
|
+
priority = {
|
|
149
|
+
"YOUTUBE_UPLOAD_VIDEO": 0,
|
|
150
|
+
"YOUTUBE_MULTIPART_UPLOAD_VIDEO": 1,
|
|
151
|
+
"YOUTUBE_UPLOAD": 2,
|
|
152
|
+
}.get(normalized, 3)
|
|
153
|
+
elif slug == "update_video":
|
|
154
|
+
priority = 0 if normalized == "YOUTUBE_UPDATE_VIDEO" else 1
|
|
155
|
+
else:
|
|
156
|
+
priority = 0 if "VIDEO" in normalized and "UPLOAD" in normalized else 1
|
|
157
|
+
return priority, candidate
|
|
158
|
+
|
|
159
|
+
return min((item for item in tools if item.get("slug")), key=score)["slug"]
|
|
160
|
+
|
|
161
|
+
|
|
122
162
|
def authorize_toolkit(api_key: str, user_id: str, toolkit: str) -> dict[str, Any]:
|
|
123
163
|
toolkit = str(toolkit or "").strip()
|
|
124
164
|
if not toolkit:
|
|
@@ -19,7 +19,7 @@ from integrations.session_info_health import check_account_session_info_health,
|
|
|
19
19
|
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
|
-
from integrations.composio_upload import ComposioUploadError, execute_upload
|
|
22
|
+
from integrations.composio_upload import ComposioUploadError, execute_upload, resolve_tool_slug
|
|
23
23
|
from hermes_ui.storage import read_json, write_json
|
|
24
24
|
from hermes_ui.languages import language_locale
|
|
25
25
|
|
|
@@ -239,7 +239,16 @@ def upload_with_default_route(
|
|
|
239
239
|
|
|
240
240
|
|
|
241
241
|
def _composio_upload(settings: dict[str, Any], *, channel: dict[str, Any], **kwargs: Any) -> IntegrationResult:
|
|
242
|
-
|
|
242
|
+
configured_slug = str(settings.get("composio_tool_slug") or "").strip()
|
|
243
|
+
try:
|
|
244
|
+
slug = resolve_tool_slug(
|
|
245
|
+
str(settings.get("composio_api_key") or ""),
|
|
246
|
+
str(settings.get("composio_user_id") or ""),
|
|
247
|
+
configured_slug,
|
|
248
|
+
str(settings.get("composio_toolkit") or ""),
|
|
249
|
+
)
|
|
250
|
+
except ComposioUploadError as exc:
|
|
251
|
+
return IntegrationResult(False, str(exc), {"status": "tool_resolution_failed", "configured_slug": configured_slug})
|
|
243
252
|
normalized_slug = slug.upper().replace("-", "_")
|
|
244
253
|
connected_account_tool = normalized_slug in {"UPLOAD_VIDEO", "UPDATE_VIDEO", "UPLOAD_TIKTOK_VIDEO", "UPLOAD_INSTAGRAM_MEDIA"}
|
|
245
254
|
youtube_upload_tool = normalized_slug in {"UPLOAD_VIDEO", "YOUTUBE_UPLOAD_VIDEO", "YOUTUBE_MULTIPART_UPLOAD_VIDEO"} or ("YOUTUBE" in normalized_slug and "UPLOAD" in normalized_slug and normalized_slug != "YOUTUBE_UPLOAD")
|
|
@@ -294,7 +303,7 @@ def _composio_upload(settings: dict[str, Any], *, channel: dict[str, Any], **kwa
|
|
|
294
303
|
result = execute_upload(
|
|
295
304
|
str(settings.get("composio_api_key") or ""),
|
|
296
305
|
str(settings.get("composio_user_id") or ""),
|
|
297
|
-
|
|
306
|
+
slug,
|
|
298
307
|
str(kwargs.get("video_path") or ""),
|
|
299
308
|
file_field,
|
|
300
309
|
json.dumps(parsed_arguments, ensure_ascii=False),
|
|
@@ -304,5 +313,5 @@ def _composio_upload(settings: dict[str, Any], *, channel: dict[str, Any], **kwa
|
|
|
304
313
|
return IntegrationResult(
|
|
305
314
|
bool(result.get("successful")),
|
|
306
315
|
str(result.get("error") or "Upload via Composio concluído."),
|
|
307
|
-
{"composio_data": result.get("data") or {}, "log_id": result.get("log_id") or "", "tool_slug": result.get("tool_slug") or slug},
|
|
316
|
+
{"composio_data": result.get("data") or {}, "log_id": result.get("log_id") or "", "tool_slug": result.get("tool_slug") or slug, "configured_tool_slug": configured_slug},
|
|
308
317
|
)
|
|
@@ -11,13 +11,9 @@ from integrations.platforms import IntegrationResult
|
|
|
11
11
|
from integrations.youtube_batch import loopback_host, loopback_port, loopback_redirect_uri, token_path as batch_token_path
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"https://www.googleapis.com/auth/youtube",
|
|
18
|
-
"https://www.googleapis.com/auth/youtube.readonly",
|
|
19
|
-
"https://www.googleapis.com/auth/yt-analytics.readonly",
|
|
20
|
-
]
|
|
14
|
+
# O fluxo de upload precisa apenas do scope de publicação; Analytics e leitura
|
|
15
|
+
# são autorizados separadamente e não devem ser enviados neste refresh OAuth.
|
|
16
|
+
AGENT_SCOPES = ["https://www.googleapis.com/auth/youtube.upload"]
|
|
21
17
|
FALLBACK_SCOPES = ["https://www.googleapis.com/auth/youtube.upload"]
|
|
22
18
|
|
|
23
19
|
|
|
@@ -137,7 +133,9 @@ class _GoogleYouTubeBase:
|
|
|
137
133
|
raw = json.loads(token_source.read_text(encoding="utf-8"))
|
|
138
134
|
if isinstance(raw, dict) and isinstance(raw.get("youtube"), dict):
|
|
139
135
|
raw = raw["youtube"]
|
|
140
|
-
|
|
136
|
+
# Não reenviar self.scopes durante o refresh: o Google deve reutilizar
|
|
137
|
+
# os scopes efectivamente concedidos no token guardado.
|
|
138
|
+
credentials = Credentials.from_authorized_user_info(raw, None)
|
|
141
139
|
granted_scopes = {str(scope).strip() for scope in (credentials.scopes or [])}
|
|
142
140
|
if "https://www.googleapis.com/auth/youtube.upload" in self.scopes and granted_scopes and "https://www.googleapis.com/auth/youtube.upload" not in granted_scopes:
|
|
143
141
|
email = _safe_text((self.account or {}).get("email")) or "a conta seleccionada"
|
|
@@ -316,7 +314,9 @@ class DirectYouTubeOAuthUploader(_GoogleYouTubeBase):
|
|
|
316
314
|
settings,
|
|
317
315
|
storage_root / "state" / "youtube_oauth_fallback_token.json",
|
|
318
316
|
FALLBACK_SCOPES,
|
|
319
|
-
|
|
317
|
+
# Não reutilizar o token do agente: pode conter scopes de leitura/Analytics
|
|
318
|
+
# que provocam invalid_scope no refresh do fallback de upload.
|
|
319
|
+
alternate_token_path=None,
|
|
320
320
|
account=account,
|
|
321
321
|
)
|
|
322
322
|
|
package/package.json
CHANGED