@danhachuel/thunderbolt 0.7.35 → 0.7.37
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 +15 -19
- package/package.json +1 -1
- package/scripts/cli.mjs +51 -2
package/app/main.py
CHANGED
|
@@ -3176,7 +3176,7 @@ def render_channels():
|
|
|
3176
3176
|
st.caption("Esta subaba usa a conta Google/YouTube seleccionada para listar os canais que ela gere. Não lê a caixa Gmail e não usa e-mails como pesquisa pública.")
|
|
3177
3177
|
accounts = [account for account in settings.get("youtube_batch_accounts", []) if isinstance(account, dict) and account.get("id")]
|
|
3178
3178
|
if not accounts:
|
|
3179
|
-
st.info("
|
|
3179
|
+
st.info("Nenhuma conta Google/YouTube disponível para a lista em lote; a lista permanece vazia.")
|
|
3180
3180
|
else:
|
|
3181
3181
|
account_ids = [str(account["id"]) for account in accounts]
|
|
3182
3182
|
account_labels = {str(account["id"]): f"{account.get('email', 'Conta Google')} — {account.get('label', 'Canais YouTube')}" for account in accounts}
|
|
@@ -3953,9 +3953,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3953
3953
|
with st.expander("Gerar Thumbnail com IA", expanded=False):
|
|
3954
3954
|
topic_for_thumbnail = str(generation_settings.get("video_subject") or "").strip()
|
|
3955
3955
|
if st.button("Gerar Thumbnail com IA", key=f"{prefix}_generate_creative", width="stretch"):
|
|
3956
|
-
if
|
|
3957
|
-
st.error("Seleccione primeiro um canal.")
|
|
3958
|
-
elif not topic_for_thumbnail:
|
|
3956
|
+
if not topic_for_thumbnail:
|
|
3959
3957
|
st.error("Preencha o Video Subject antes de gerar a thumbnail.")
|
|
3960
3958
|
else:
|
|
3961
3959
|
try:
|
|
@@ -3963,7 +3961,7 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
3963
3961
|
existing_title = str(existing_payload.get("title") or topic_for_thumbnail).strip()
|
|
3964
3962
|
generated = generate_thumbnail_variants_for_ui(
|
|
3965
3963
|
read_json("settings.json", {}),
|
|
3966
|
-
selected_one,
|
|
3964
|
+
selected_one or {},
|
|
3967
3965
|
topic_for_thumbnail,
|
|
3968
3966
|
same_channel_quantity,
|
|
3969
3967
|
title=existing_title,
|
|
@@ -6166,8 +6164,7 @@ def render_tiktok_automation():
|
|
|
6166
6164
|
channels = _tiktok_channel_records()
|
|
6167
6165
|
prompt_ids, prompt_labels = _tiktok_prompt_options()
|
|
6168
6166
|
if not channels:
|
|
6169
|
-
st.info("
|
|
6170
|
-
return
|
|
6167
|
+
st.info("Nenhum canal TikTok seleccionado. A lista de automação permanece vazia até existir um destino.")
|
|
6171
6168
|
with st.expander("Canais cadastrados", expanded=False):
|
|
6172
6169
|
for channel in channels:
|
|
6173
6170
|
channel_id = str(channel["id"])
|
|
@@ -6426,8 +6423,7 @@ def render_facebook_automation() -> None:
|
|
|
6426
6423
|
pages = _facebook_pages_for_automation()
|
|
6427
6424
|
settings = read_json("settings.json", {})
|
|
6428
6425
|
if not pages:
|
|
6429
|
-
st.
|
|
6430
|
-
return
|
|
6426
|
+
st.info("Nenhuma Facebook Page seleccionada. A fila pode ser criada e preenchida; o destino poderá ser escolhido mais tarde.")
|
|
6431
6427
|
with st.expander("Configurar API Facebook da página", expanded=False):
|
|
6432
6428
|
for page in pages:
|
|
6433
6429
|
page_id = str(page.get("id") or "")
|
|
@@ -6440,9 +6436,9 @@ def render_facebook_automation() -> None:
|
|
|
6440
6436
|
st.success("Credenciais da página guardadas localmente.")
|
|
6441
6437
|
st.rerun()
|
|
6442
6438
|
with st.form("facebook_automation_new_post"):
|
|
6443
|
-
page_options = [str(page.get("id")) for page in pages]
|
|
6439
|
+
page_options = [""] + [str(page.get("id")) for page in pages]
|
|
6444
6440
|
page_labels = {str(page.get("id")): str(page.get("name") or "Facebook Page") for page in pages}
|
|
6445
|
-
selected_page = st.selectbox("Facebook Page", page_options, format_func=lambda value: page_labels.get(value,
|
|
6441
|
+
selected_page = st.selectbox("Facebook Page", page_options, format_func=lambda value: page_labels.get(value, "Nenhuma página seleccionada"), key="facebook_automation_target_page")
|
|
6446
6442
|
form_cols = st.columns([2.2, 1, 1.3])
|
|
6447
6443
|
with form_cols[0]:
|
|
6448
6444
|
theme = st.text_input("Tema/ideia opcional", placeholder="Deixe vazio para o LLM sugerir um tema")
|
|
@@ -6452,7 +6448,7 @@ def render_facebook_automation() -> None:
|
|
|
6452
6448
|
start_stage = st.selectbox("Iniciar por", ["Ideia / Tema", "Tema já definido"])
|
|
6453
6449
|
create_clicked = st.form_submit_button("Criar post na fila", type="primary", width="stretch")
|
|
6454
6450
|
if create_clicked:
|
|
6455
|
-
page = next(page for page in pages if str(page.get("id")) == selected_page)
|
|
6451
|
+
page = next((page for page in pages if str(page.get("id")) == selected_page), {})
|
|
6456
6452
|
post = create_post(page, image_count=int(image_count), theme=theme)
|
|
6457
6453
|
if start_stage == "Tema já definido" and theme.strip():
|
|
6458
6454
|
save_post({**post, "status": "artigo_pendente"})
|
|
@@ -7280,15 +7276,15 @@ def render_upload_destination_target(destination: str, channels: list[dict[str,
|
|
|
7280
7276
|
select_label = "Canal" if destination == "YouTube" else ("Conta TikTok" if destination == "TikTok" else ("Conta Bilibili" if destination == "Bilibili" else "Perfil / página"))
|
|
7281
7277
|
empty_label = "Nenhum canal YouTube cadastrado" if destination == "YouTube" else ("Nenhuma conta TikTok cadastrada" if destination == "TikTok" else ("Nenhuma conta Bilibili activa" if destination == "Bilibili" else f"Nenhum {destination} configurado"))
|
|
7282
7278
|
if not options:
|
|
7283
|
-
st.selectbox(select_label, [
|
|
7279
|
+
st.selectbox(select_label, [""], format_func=lambda _value: empty_label, key=f"upload_target_{destination_key}")
|
|
7284
7280
|
if destination == "YouTube":
|
|
7285
|
-
st.caption("
|
|
7281
|
+
st.caption("Nenhum canal YouTube disponível; o destino permanece vazio e pode ser escolhido mais tarde.")
|
|
7286
7282
|
elif destination == "TikTok":
|
|
7287
|
-
st.caption("
|
|
7283
|
+
st.caption("Nenhuma conta TikTok disponível; o destino permanece vazio e pode ser escolhido mais tarde.")
|
|
7288
7284
|
elif destination == "Bilibili":
|
|
7289
|
-
st.caption("
|
|
7285
|
+
st.caption("Nenhuma conta Bilibili disponível; o destino permanece vazio e pode ser escolhido mais tarde.")
|
|
7290
7286
|
else:
|
|
7291
|
-
st.caption(f"
|
|
7287
|
+
st.caption(f"Nenhum destino {destination} disponível; a lista permanece vazia.")
|
|
7292
7288
|
return None
|
|
7293
7289
|
return st.selectbox(
|
|
7294
7290
|
select_label,
|
|
@@ -7413,8 +7409,8 @@ def render_upload_conventional():
|
|
|
7413
7409
|
quota_count = official_upload_count(channel, account)
|
|
7414
7410
|
st.caption(f"API Oficial hoje: {quota_count}/{OFFICIAL_DAILY_LIMIT} envios nesta conta Gmail.")
|
|
7415
7411
|
if not selected_youtube_channel:
|
|
7416
|
-
st.caption("
|
|
7417
|
-
if st.button("Enviar pelo fluxo recomendado", type="primary", key=f"upload_youtube_{task['id']}"
|
|
7412
|
+
st.caption("Nenhum canal YouTube seleccionado; o destino permanece vazio e poderá ser escolhido mais tarde.")
|
|
7413
|
+
if st.button("Enviar pelo fluxo recomendado", type="primary", key=f"upload_youtube_{task['id']}"):
|
|
7418
7414
|
tags = [tag.strip() for tag in tags_raw.split(",") if tag.strip()]
|
|
7419
7415
|
result = upload_with_default_route(
|
|
7420
7416
|
settings,
|
package/package.json
CHANGED
package/scripts/cli.mjs
CHANGED
|
@@ -9,6 +9,10 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
|
|
10
10
|
const root = resolve(fileURLToPath(new URL(".", import.meta.url)), "..");
|
|
11
11
|
const args = process.argv.slice(2);
|
|
12
|
+
const packageVersion = (() => {
|
|
13
|
+
try { return JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version || "dev"; }
|
|
14
|
+
catch { return "dev"; }
|
|
15
|
+
})();
|
|
12
16
|
|
|
13
17
|
function getUserHome() {
|
|
14
18
|
if (platform() !== "win32") return homedir();
|
|
@@ -200,6 +204,33 @@ const backendPort = await new Promise((resolve, reject) => {
|
|
|
200
204
|
});
|
|
201
205
|
});
|
|
202
206
|
const supportedLanguages = new Set(["en", "zh", "de", "vi", "tr", "pt", "ru", "es", "id", "it"]);
|
|
207
|
+
const dynamicChunkRecoveryScript = `<script data-thunderbolt-recovery="${packageVersion}">
|
|
208
|
+
(() => {
|
|
209
|
+
const key = "thunderbolt-dynamic-chunk-recovery";
|
|
210
|
+
const recover = () => {
|
|
211
|
+
try {
|
|
212
|
+
const now = Date.now();
|
|
213
|
+
const previous = Number(sessionStorage.getItem(key) || 0);
|
|
214
|
+
if (now - previous < 30000) return;
|
|
215
|
+
sessionStorage.setItem(key, String(now));
|
|
216
|
+
const url = new URL(window.location.href);
|
|
217
|
+
url.searchParams.set("tb_refresh", String(now));
|
|
218
|
+
window.location.replace(url.href);
|
|
219
|
+
} catch (_) {
|
|
220
|
+
window.location.reload();
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
224
|
+
const message = String(event.reason?.message || event.reason || "");
|
|
225
|
+
if (/Failed to fetch dynamically imported module|Importing a module script failed/i.test(message)) recover();
|
|
226
|
+
});
|
|
227
|
+
window.addEventListener("error", (event) => {
|
|
228
|
+
const message = String(event.message || "");
|
|
229
|
+
if (/dynamically imported module|module script failed/i.test(message)) recover();
|
|
230
|
+
});
|
|
231
|
+
window.setTimeout(() => { try { sessionStorage.removeItem(key); } catch (_) {} }, 3500);
|
|
232
|
+
})();
|
|
233
|
+
</script>`;
|
|
203
234
|
|
|
204
235
|
const proxy = http.createServer((request, response) => {
|
|
205
236
|
const requestUrl = new URL(request.url || "/", `http://localhost:${publicPort}`);
|
|
@@ -240,8 +271,26 @@ const proxy = http.createServer((request, response) => {
|
|
|
240
271
|
responseHeaders["cache-control"] = "no-store, no-cache, must-revalidate, max-age=0";
|
|
241
272
|
responseHeaders.pragma = "no-cache";
|
|
242
273
|
responseHeaders.expires = "0";
|
|
243
|
-
|
|
244
|
-
|
|
274
|
+
const contentType = String(responseHeaders["content-type"] || "").toLowerCase();
|
|
275
|
+
const isHtml = contentType.includes("text/html") && !responseHeaders["content-encoding"];
|
|
276
|
+
if (!isHtml) {
|
|
277
|
+
response.writeHead(upstreamResponse.statusCode || 502, responseHeaders);
|
|
278
|
+
upstreamResponse.pipe(response);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const chunks = [];
|
|
282
|
+
let totalBytes = 0;
|
|
283
|
+
upstreamResponse.on("data", (chunk) => {
|
|
284
|
+
chunks.push(chunk);
|
|
285
|
+
totalBytes += chunk.length;
|
|
286
|
+
});
|
|
287
|
+
upstreamResponse.on("end", () => {
|
|
288
|
+
let body = Buffer.concat(chunks, totalBytes).toString("utf8");
|
|
289
|
+
if (body.includes("</body>")) body = body.replace("</body>", `${dynamicChunkRecoveryScript}</body>`);
|
|
290
|
+
delete responseHeaders["content-length"];
|
|
291
|
+
response.writeHead(upstreamResponse.statusCode || 502, responseHeaders);
|
|
292
|
+
response.end(body);
|
|
293
|
+
});
|
|
245
294
|
});
|
|
246
295
|
upstream.on("error", (error) => {
|
|
247
296
|
response.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
|