@danhachuel/thunderbolt 0.5.17 → 0.5.19
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/hermes_ui/canva_mcp_workflow.py +56 -14
- package/package.json +1 -1
|
@@ -71,24 +71,50 @@ def _first_id(value: Any, keys: tuple[str, ...]) -> str:
|
|
|
71
71
|
return ""
|
|
72
72
|
|
|
73
73
|
|
|
74
|
-
def _first_text_element(value: Any) -> str:
|
|
74
|
+
def _first_text_element(value: Any, *, text_context: bool = False) -> str:
|
|
75
75
|
if isinstance(value, Mapping):
|
|
76
|
-
|
|
76
|
+
# The MCP returns rich text elements with slightly different ID
|
|
77
|
+
# names depending on the design type/version of the server.
|
|
78
|
+
text_markers = ("text", "richtext", "rich_text", "content", "paragraph")
|
|
79
|
+
mapping_text = str(value).lower()
|
|
80
|
+
is_text_mapping = text_context or any(
|
|
81
|
+
marker in str(key).lower() or marker in mapping_text
|
|
82
|
+
for key in value
|
|
83
|
+
for marker in text_markers
|
|
84
|
+
)
|
|
85
|
+
for key in ("element_id", "elementId", "richtext_id", "richTextId", "text_id", "textId", "id"):
|
|
77
86
|
candidate = str(value.get(key) or "").strip()
|
|
78
|
-
if candidate and
|
|
87
|
+
if candidate and is_text_mapping:
|
|
79
88
|
return candidate
|
|
80
|
-
for child in value.
|
|
81
|
-
|
|
89
|
+
for key, child in value.items():
|
|
90
|
+
child_context = text_context or any(marker in str(key).lower() for marker in text_markers)
|
|
91
|
+
found = _first_text_element(child, text_context=child_context)
|
|
82
92
|
if found:
|
|
83
93
|
return found
|
|
84
94
|
elif isinstance(value, list):
|
|
85
95
|
for child in value:
|
|
86
|
-
found = _first_text_element(child)
|
|
96
|
+
found = _first_text_element(child, text_context=text_context)
|
|
87
97
|
if found:
|
|
88
98
|
return found
|
|
89
99
|
return ""
|
|
90
100
|
|
|
91
101
|
|
|
102
|
+
def _has_richtext(value: Any) -> bool:
|
|
103
|
+
"""Return whether a Canva payload contains at least one richtext item."""
|
|
104
|
+
if isinstance(value, Mapping):
|
|
105
|
+
for key, child in value.items():
|
|
106
|
+
if str(key).lower() in {"richtexts", "rich_texts", "text_elements", "textelements"}:
|
|
107
|
+
if isinstance(child, list) and any(isinstance(item, Mapping) for item in child):
|
|
108
|
+
return True
|
|
109
|
+
if isinstance(child, Mapping) and child:
|
|
110
|
+
return True
|
|
111
|
+
if _has_richtext(child):
|
|
112
|
+
return True
|
|
113
|
+
elif isinstance(value, list):
|
|
114
|
+
return any(_has_richtext(child) for child in value)
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
|
|
92
118
|
def _first_page(value: Any) -> tuple[str, list[dict[str, Any]]]:
|
|
93
119
|
pages: list[dict[str, Any]] = []
|
|
94
120
|
if isinstance(value, Mapping) and isinstance(value.get("pages"), list):
|
|
@@ -122,17 +148,27 @@ def run_direct_canva_thumbnail(
|
|
|
122
148
|
candidates = _items(result)
|
|
123
149
|
if not candidates:
|
|
124
150
|
raise CanvaMCPError("Canva não encontrou designs para as palavras-chave da thumbnail.")
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if not design_id:
|
|
151
|
+
candidates = [candidate for candidate in candidates if _design_id(candidate)]
|
|
152
|
+
if not candidates:
|
|
128
153
|
raise CanvaMCPError("A pesquisa Canva devolveu um design sem ID.")
|
|
129
154
|
|
|
130
155
|
get_name = "get-design-content" if "get-design-content" in available else "get_design_content"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
156
|
+
selected = None
|
|
157
|
+
content: Any = {}
|
|
158
|
+
for candidate in candidates:
|
|
159
|
+
candidate_id = _design_id(candidate)
|
|
160
|
+
candidate_content = _payload(client.call(get_name, {
|
|
161
|
+
"design_id": candidate_id,
|
|
162
|
+
"content_types": ["richtexts"],
|
|
163
|
+
"user_intent": "Inspect the selected Canva design before applying the thumbnail edit.",
|
|
164
|
+
}))
|
|
165
|
+
if _has_richtext(candidate_content):
|
|
166
|
+
selected = candidate
|
|
167
|
+
content = candidate_content
|
|
168
|
+
break
|
|
169
|
+
if selected is None:
|
|
170
|
+
raise CanvaMCPError("Os designs Canva encontrados não têm elementos de texto editáveis.")
|
|
171
|
+
design_id = _design_id(selected)
|
|
136
172
|
start_name = "start-editing-transaction" if "start-editing-transaction" in available else "start_editing_transaction"
|
|
137
173
|
started = _payload(client.call(start_name, {
|
|
138
174
|
"design_id": design_id,
|
|
@@ -144,6 +180,12 @@ def run_direct_canva_thumbnail(
|
|
|
144
180
|
page_id, pages = _first_page(started)
|
|
145
181
|
element_id = _first_text_element(started) or _first_text_element(content)
|
|
146
182
|
if not element_id:
|
|
183
|
+
cancel_name = "cancel-editing-transaction" if "cancel-editing-transaction" in available else "cancel_editing_transaction"
|
|
184
|
+
if cancel_name in available:
|
|
185
|
+
client.call(cancel_name, {
|
|
186
|
+
"transaction_id": transaction_id,
|
|
187
|
+
"user_intent": "Discard the Canva editing transaction because no editable text element was returned.",
|
|
188
|
+
})
|
|
147
189
|
raise CanvaMCPError("O design Canva não devolveu um elemento de texto editável.")
|
|
148
190
|
perform_name = "perform-editing-operations" if "perform-editing-operations" in available else "perform_editing_operations"
|
|
149
191
|
operations = [{"type": "replace_text", "element_id": element_id, "text": title[:255]}]
|
package/package.json
CHANGED