@danhachuel/thunderbolt 0.3.33 → 0.3.35
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/MANUAL-INSTALACAO.md +16 -6
- package/README.md +8 -8
- package/app/main.py +156 -83
- package/hermes_ui/api_key_tests.py +12 -0
- package/hermes_ui/languages.py +21 -0
- package/hermes_ui/notifications.py +22 -0
- package/hermes_ui/storage.py +5 -0
- package/integrations/telegram_gateway.py +158 -0
- package/package.json +2 -1
- package/requirements.txt +1 -1
- package/scripts/npx-cache-compat.mjs +15 -0
package/MANUAL-INSTALACAO.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Este manual descreve a instalação local da UI Thunderbolt, baseada no MoneyPrinterTurbo, utilizando o pacote npm `@danhachuel/thunderbolt`. O fluxo recomendado instala automaticamente o ambiente Python, as dependências da aplicação, as dependências do MoneyPrinterTurbo, o Streamlit e o suporte FFmpeg através de `imageio-ffmpeg`.
|
|
4
4
|
|
|
5
|
-
> **Versão deste manual:** 0.
|
|
5
|
+
> **Versão deste manual:** 0.3.35
|
|
6
6
|
> **Pacote npm:** `@danhachuel/thunderbolt`
|
|
7
7
|
> **Porta padrão da UI:** `localhost:3030`
|
|
8
8
|
> **Repositório:** [github.com/DanHachuel/thunderbolt](https://github.com/DanHachuel/thunderbolt)
|
|
@@ -100,13 +100,13 @@ Execute:
|
|
|
100
100
|
Windows PowerShell ou MobaXterm:
|
|
101
101
|
|
|
102
102
|
```powershell
|
|
103
|
-
npx.cmd --yes @danhachuel/thunderbolt@0.
|
|
103
|
+
npx.cmd --yes @danhachuel/thunderbolt@0.3.35 install
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
Linux/macOS:
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
|
-
npx --yes @danhachuel/thunderbolt@0.
|
|
109
|
+
npx --yes @danhachuel/thunderbolt@0.3.35 install
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
A instalação normal é **segura para actualizações**: preserva `storage`, Blueprints, Brandings, configurações e artefactos do utilizador. Remove apenas `.venv`, o clone técnico do MoneyPrinterTurbo e dependências que serão recriadas. Uma pasta antiga sem dados do utilizador, como `C:\Users\<utilizador>\AppData\Local\hermes` da tentativa incompleta, pode ser removida; uma pasta antiga que contenha Blueprints, Brandings ou storage é preservada e apenas avisada no terminal. Feche processos Python, Node, Streamlit e MobaXterm que estejam a usar as pastas antes de executar.
|
|
@@ -196,7 +196,7 @@ O Thunderbolt inclui as dez opções de idioma usadas pelo fluxo MoneyPrinterTur
|
|
|
196
196
|
|
|
197
197
|
No topo da área principal, o menu nativo de idioma segue o padrão do MoneyPrinterTurbo e mostra o rótulo **Language** acima do selector. Cada opção apresenta o nome e o código, acompanhados por uma imagem SVG local da bandeira; não depende de emojis nem de siglas da fonte do sistema. A escolha fica guardada como `ui_language` em `storage/state/settings.json` e, quando existe um caminho MoneyPrinterTurbo configurado, é sincronizada com `[ui].language` no `config.toml`. O dashboard inicial, o interior das páginas e as abas da barra lateral usam o mesmo idioma seleccionado. O toolbar do Streamlit não é manipulado: o indicador de execução, **Deploy** e o menu de três pontos permanecem nativos e clicáveis. Na Criação de Vídeos e em Roteiros, o selector **Script Language** mantém o catálogo de vídeo independente, mas guarda o código curto; essa escolha é sincronizada com `[ui].video_language`.
|
|
198
198
|
|
|
199
|
-
Todas as subabas internas e o conteúdo das páginas também são traduzidos nos dez idiomas. Isso inclui títulos, subtítulos, descrições, labels, placeholders, opções, botões, avisos, mensagens de sucesso/erro, estados vazios, métricas, expanderes e blocos Markdown/HTML. Valores técnicos, IDs, URLs, nomes de ficheiros e dados introduzidos pelo utilizador permanecem inalterados. As subabas internas traduzidas incluem: Blueprints/Brandings, Pesquisa pública/Cadastro manual/Contas cadastradas, Upload/Biblioteca, Importar do YouTube/Canais em lote gmail/Cadastro manual, Criar vídeo/Vídeos, Novo roteiro/letra/Histórico guardado, análise de clusters, edição de cortes, Vídeos/Código Python, as quatro opções de Upload, API Keys/Teste de vozes, Serviços e modelos/Fontes de materiais
|
|
199
|
+
Todas as subabas internas e o conteúdo das páginas também são traduzidos nos dez idiomas. Isso inclui títulos, subtítulos, descrições, labels, placeholders, opções, botões, avisos, mensagens de sucesso/erro, estados vazios, métricas, expanderes e blocos Markdown/HTML. Valores técnicos, IDs, URLs, nomes de ficheiros e dados introduzidos pelo utilizador permanecem inalterados. As subabas internas traduzidas incluem: Blueprints/Brandings, Pesquisa pública/Cadastro manual/Contas cadastradas, Upload/Biblioteca, Importar do YouTube/Canais em lote gmail/Cadastro manual, Criar vídeo/Vídeos, Novo roteiro/letra/Histórico guardado, análise de clusters, edição de cortes, Vídeos/Código Python, as quatro opções de Upload, API Keys/Teste de vozes, Serviços e modelos/Fontes de materiais, Client MCP/Servidor MCP/Skill e Notificações Geral/Telegram. A ordem e as chaves internas dos widgets permanecem estáveis.
|
|
200
200
|
|
|
201
201
|
### Temas Light e Dark
|
|
202
202
|
|
|
@@ -357,7 +357,7 @@ Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
|
|
|
357
357
|
|
|
358
358
|
## 8. Configuração inicial da UI
|
|
359
359
|
|
|
360
|
-
Na primeira execução, a barra lateral apresenta **Início**, **Niche Finder**, **Pipeline**, **Pipeline TikTok**, **Automação**, **Edição**, **AI Influencers** e **Configurações**. O menu expansível **Automação** contém a subaba **Automação Youtube**. O menu expansível **Edição** contém **Limpador de Metadados**, **Cortes** e **Editor Python**. O menu expansível **AI Influencers** contém **Personagens**, **Redes Sociais**, **Tutorial Meta** e **Tutorial Supabase**, nessa ordem. O **Tutorial Meta** apresenta o guia de configuração de Instagram e credenciais Meta para automações com n8n. O **Tutorial Supabase** apresenta o guia para criar as tabelas `plans` e `posts` e o bucket `instagram-images`, incluindo a ligação para a fonte original no GitHub; as duas frases promocionais da comunidade foram omitidas. O menu expansível **Niche Finder** contém **Niche Finder Kaggle** e **Niche Finder Apify**. A aba **Niche Finder Kaggle** mantém a análise do dataset Kaggle; **Niche Finder Apify** é uma segunda alternativa independente, com actor, dataset, parâmetros, credencial, execução e resultados próprios. O menu **Pipeline** contém **Criação de Vídeos**, **Criação de Músicas**, **Roteiros** e **Upload**. O menu **Configurações** contém **Canais Youtube**, **Blueprints Youtube**, **MCP**, **Contas Google**, **Configuração API** e **Notificações**. Abra **Configurações > Contas Google** para gerir contas Google/YouTube, cartões, documentos de Upload directo, `sessionInfo` por conta, a `INNERTUBE_API_KEY` global, contas adicionais e a configuração global do YouTube. Abra **Configurações > Configuração API** para configurar as restantes API Keys, providers, modelos, serviços e o **Teste de vozes**. A página **Configurações > Notificações**
|
|
360
|
+
Na primeira execução, a barra lateral apresenta **Início**, **Niche Finder**, **Pipeline**, **Pipeline TikTok**, **Automação**, **Edição**, **AI Influencers** e **Configurações**. O menu expansível **Automação** contém a subaba **Automação Youtube**. O menu expansível **Edição** contém **Limpador de Metadados**, **Cortes** e **Editor Python**. O menu expansível **AI Influencers** contém **Personagens**, **Redes Sociais**, **Tutorial Meta** e **Tutorial Supabase**, nessa ordem. O **Tutorial Meta** apresenta o guia de configuração de Instagram e credenciais Meta para automações com n8n. O **Tutorial Supabase** apresenta o guia para criar as tabelas `plans` e `posts` e o bucket `instagram-images`, incluindo a ligação para a fonte original no GitHub; as duas frases promocionais da comunidade foram omitidas. O menu expansível **Niche Finder** contém **Niche Finder Kaggle** e **Niche Finder Apify**. A aba **Niche Finder Kaggle** mantém a análise do dataset Kaggle; **Niche Finder Apify** é uma segunda alternativa independente, com actor, dataset, parâmetros, credencial, execução e resultados próprios. O menu **Pipeline** contém **Criação de Vídeos**, **Criação de Músicas**, **Roteiros** e **Upload**. O menu **Configurações** contém **Canais Youtube**, **Blueprints Youtube**, **MCP**, **Contas Google**, **Configuração API** e **Notificações**. Abra **Configurações > Contas Google** para gerir contas Google/YouTube, cartões, documentos de Upload directo, `sessionInfo` por conta, a `INNERTUBE_API_KEY` global, contas adicionais e a configuração global do YouTube. Abra **Configurações > Configuração API** para configurar as restantes API Keys, providers, modelos, serviços e o **Teste de vozes**. A página **Configurações > Notificações** contém as subabas **Geral** e **Telegram**. A subaba Geral mantém o histórico e as preferências dos eventos locais; a subaba Telegram envia esses mesmos eventos para o Chat ID configurado. Para YouTube, preencha primeiro o par **OAuth Client ID + OAuth Client Secret** em **Contas Google** se pretende autorizar uploads. A **YouTube Data API Key** é uma credencial Google Cloud diferente e fica em **Contas Google**, apenas para o método oficial de métricas; Client ID + Client Secret não formam uma API Key.
|
|
361
361
|
|
|
362
362
|
| Configuração | Finalidade |
|
|
363
363
|
|---|---|
|
|
@@ -378,6 +378,16 @@ Na primeira execução, a barra lateral apresenta **Início**, **Niche Finder**,
|
|
|
378
378
|
| Apify API Token | Token pessoal da Apify, guardado mascarado no storage local e usado apenas pela alternativa Apify |
|
|
379
379
|
| Apify Actor ID | Actor usado pela alternativa Apify, por padrão `streamers~youtube-scraper` |
|
|
380
380
|
| Apify polling/timeout | Intervalo de consulta e limite máximo da execução do actor |
|
|
381
|
+
| Telegram Bot Token | Token criado pelo BotFather, guardado apenas no storage local |
|
|
382
|
+
| Telegram Chat ID | Utilizador, grupo ou canal que receberá as notificações |
|
|
383
|
+
| Telegram Proxy | Proxy HTTP/HTTPS/SOCKS opcional para ambientes sem acesso directo |
|
|
384
|
+
| Telegram timeout | Limite de espera de cada envio, entre 5 e 120 segundos |
|
|
385
|
+
|
|
386
|
+
### Telegram Gateway
|
|
387
|
+
|
|
388
|
+
Em **Configurações > Notificações > Telegram**, active **Activar notificações Telegram**, introduza o **Telegram Bot Token** criado pelo BotFather e o **Telegram Chat ID** de destino. O botão **Testar chamada API** executa apenas o método read-only `getMe` para validar o bot; não envia uma mensagem de teste. O proxy é opcional e o suporte SOCKS depende das dependências instaladas pelo `requirements.txt`.
|
|
389
|
+
|
|
390
|
+
Depois de guardar, cada nova notificação permitida pelas preferências da subaba **Geral** é enviada pela Bot API oficial do Telegram através de `sendMessage`. A notificação continua a ser persistida localmente mesmo que o Telegram esteja indisponível, o Chat ID esteja errado ou a API devolva um erro; a falha externa nunca interrompe a produção, os uploads ou a UI. A integração é apenas outbound: não recebe comandos, não faz polling e não expõe o Bot Token em logs, metadados ou no GitHub.
|
|
381
391
|
|
|
382
392
|
As credenciais devem ser inseridas apenas na configuração local. A Kaggle API Key pertence exclusivamente à alternativa Kaggle. O Apify API Token pertence exclusivamente à alternativa Apify e não é usado para Kaggle; os resultados e histórico Apify ficam separados em `niche_apify_runs.json`. A Data API Key, o OAuth Client ID e o OAuth Client Secret são valores diferentes; Client ID + Secret não geram uma API Key nem um token OAuth até a conta ser autorizada. Não coloque nenhum deles no GitHub, no `package.json`, em blueprints ou em ficheiros de estado versionados.
|
|
383
393
|
|
|
@@ -448,7 +458,7 @@ Ao abrir a página, o Thunderbolt não prepara dados públicos, não descarrega
|
|
|
448
458
|
|
|
449
459
|
Os parâmetros da UI são número de clusters entre 2 e 10, suporte mínimo entre 0,01 e 0,50, país, engagement, intervalo de datas e tags, todos dentro da área principal da aba. O núcleo normaliza os dados, calcula engagement, aplica filtros, faz transformação logarítmica e standardização, executa K-Means e calcula itemsets/regras com FP-Growth. Não são apresentados resultados até ao primeiro clique em **Analisar Nichos**; o mesmo botão aplica alterações posteriores aos filtros. Os resultados são DataFrames de clusters, itemsets frequentes, regras de associação e dados analisados; o gráfico de dispersão é criado nativamente com Plotly.
|
|
450
460
|
|
|
451
|
-
As dependências adicionais — `scikit-learn`, `mlxtend`, `plotly`, `seaborn`, `matplotlib` e `kagglehub` — são instaladas pelo procedimento normal de `npx`. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.
|
|
461
|
+
As dependências adicionais — `scikit-learn`, `mlxtend`, `plotly`, `seaborn`, `matplotlib` e `kagglehub` — são instaladas pelo procedimento normal de `npx`. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.3.35 install`; o instalador detecta e reutiliza o que já estiver válido.
|
|
452
462
|
|
|
453
463
|
### Niche Finder Apify
|
|
454
464
|
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ A primeira versão implementa a camada UI independente com:
|
|
|
22
22
|
| Edição | Menu expansível abaixo de Automação com Limpador de Metadados, Clip Generator local em Cortes e Editor Python inspirado no PYEdit para vídeos e scripts locais |
|
|
23
23
|
| Upload | YouTube via `youtube-automation-agent` adaptado internamente, fallback ordenado API Oficial → Upload directo → Postiz, upload de MP4 para Postiz via API key/MCP configurável, TikTok, Instagram e Facebook Pages no front end |
|
|
24
24
|
| MCP | Catálogo local opcional de Short Video Maker, AutoVio, OpenMontage e OpenCut, com portas editáveis e activação |
|
|
25
|
-
| Configurações | **Contas Google**, **Configuração API** e **Notificações**; API Keys divididas entre **Serviços e modelos** e **Fontes de materiais**, com várias chaves independentes por fonte, além de contas Google/YouTube por cartão, `INNERTUBE_API_KEY` global para todas as contas, providers, TTS, Nano Banana, Postiz, TikTok e Upload-Post |
|
|
25
|
+
| Configurações | **Contas Google**, **Configuração API** e **Notificações**; Notificações contém as subabas **Geral** e **Telegram Gateway**, com envio das mesmas operações locais para um Chat ID; API Keys divididas entre **Serviços e modelos** e **Fontes de materiais**, com várias chaves independentes por fonte, além de contas Google/YouTube por cartão, `INNERTUBE_API_KEY` global para todas as contas, providers, TTS, Nano Banana, Postiz, TikTok e Upload-Post |
|
|
26
26
|
| Launcher | Execução via `npx`, instalação assistida, diagnóstico e preparação para distribuição |
|
|
27
27
|
|
|
28
28
|
## Upload-Post — publicação para múltiplas plataformas
|
|
@@ -56,7 +56,7 @@ No topo da área principal da aplicação existe o menu nativo de idioma no padr
|
|
|
56
56
|
|
|
57
57
|
A UI suporta os temas **Dark** e **Light** através do menu nativo de três pontos do Streamlit, no local original do toolbar. Não existe um selector Theme adicional dentro da página. A configuração distribuída em `.streamlit/config.toml` define **Dark** como base inicial e o menu nativo continua responsável por alternar entre os modos, seguindo o padrão do [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo). O CSS próprio do Thunderbolt usa cores semânticas, `currentColor` e `color-mix` para acompanhar o tema activo, sem alterar a posição nem a funcionalidade do toolbar, do botão Deploy e do menu principal.
|
|
58
58
|
|
|
59
|
-
## Navegação da UI 0.
|
|
59
|
+
## Navegação da UI 0.3.35
|
|
60
60
|
|
|
61
61
|
A barra lateral mantém os níveis principais, nesta ordem: **Início**, **Niche Finder**, **Pipeline**, **Pipeline TikTok**, **Automação**, **Edição**, **AI Influencers** e **Configurações**. **Pipeline** é expansível e contém **Criação de Vídeos**, **Criação de Músicas**, **Roteiros** e **Upload**. **Automação** também é expansível e contém **Automação Youtube**. **Edição** é expansível e contém **Limpador de Metadados**, **Cortes**, **Editor Python** e **Download Mídia**, nessa ordem. **AI Influencers** é expansível e contém **Personagens**, **Redes Sociais**, **Tutorial Meta** e **Tutorial Supabase**, nessa ordem. **Niche Finder** é expansível e contém **Niche Finder Kaggle** e **Niche Finder Apify**. **Configurações** é expansível e contém **Canais Youtube**, **Blueprints Youtube**, **MCP**, **Contas Google**, **Configuração API** e **Notificações**. O Início reúne o dashboard e as filas do Pipeline, sem botões de acções rápidas.
|
|
62
62
|
|
|
@@ -64,7 +64,7 @@ A subaba **Tutorial Supabase** apresenta o guia de criação das tabelas `plans`
|
|
|
64
64
|
|
|
65
65
|
O conteúdo interno das páginas também é localizado, não apenas a navegação: títulos, subtítulos, descrições, labels de campos, placeholders, opções de selectores, botões, avisos, mensagens de sucesso/erro, estados vazios, métricas, expanderes e blocos Markdown/HTML são traduzidos no idioma seleccionado através da camada global de conteúdo. Valores técnicos, chaves de estado, IDs de formulários, nomes de ficheiros, URLs e dados introduzidos pelo utilizador permanecem inalterados.
|
|
66
66
|
|
|
67
|
-
Todas as subabas internas também são localizadas pelo helper nativo de tabs: **Blueprints/Brandings**, **Pesquisa pública/Cadastro manual/Contas cadastradas**, **Upload/Biblioteca**, **Importar do YouTube/Canais em lote gmail/Cadastro manual**, **Criar vídeo/Vídeos**, **Novo roteiro/letra/Histórico guardado**, as três abas de análise de clusters, as quatro abas de cortes, **Vídeos/Código Python**, as quatro opções de Upload, **API Keys/Teste de vozes**, **Serviços e modelos/Fontes de materiais
|
|
67
|
+
Todas as subabas internas também são localizadas pelo helper nativo de tabs: **Blueprints/Brandings**, **Pesquisa pública/Cadastro manual/Contas cadastradas**, **Upload/Biblioteca**, **Importar do YouTube/Canais em lote gmail/Cadastro manual**, **Criar vídeo/Vídeos**, **Novo roteiro/letra/Histórico guardado**, as três abas de análise de clusters, as quatro abas de cortes, **Vídeos/Código Python**, as quatro opções de Upload, **API Keys/Teste de vozes**, **Serviços e modelos/Fontes de materiais**, **Client MCP/Servidor MCP/Skill** e **Notificações/Geral/Telegram**. As chaves técnicas e a ordem funcional permanecem inalteradas.
|
|
68
68
|
|
|
69
69
|
Dentro de **Configurações > Contas Google**, a UI contém os cartões expansíveis de contas Google/YouTube, `sessionInfo` por conta, documentos de Upload directo, o formulário **Adicionar outra conta Gmail**, o bloco global de `INNERTUBE_API_KEY` e a configuração global do YouTube (OAuth Client ID, OAuth Client Secret e YouTube Data API Key). A página **Configuração API** contém as restantes credenciais, providers, modelos, serviços, Nano Banana, TikTok e Postiz.
|
|
70
70
|
|
|
@@ -72,7 +72,7 @@ Em **Configuração API > API Keys**, a subaba **Fontes de materiais** segue o p
|
|
|
72
72
|
|
|
73
73
|
A subaba **Serviços e modelos** mantém as credenciais de providers e serviços que não são fontes de materiais. A chave `gemini_image_api_key` da Nano Banana continua separada de `gemini_api_key` do LLM textual. `INNERTUBE_API_KEY` permanece exclusivamente em **Configurações > Contas Google**, no bloco de configuração global fora dos cartões, e não é duplicada em API Keys.
|
|
74
74
|
|
|
75
|
-
A página **AI Influencers > Tutorial Meta** apresenta o guia de configuração de uma conta Instagram profissional e das credenciais Meta para automações com n8n, distribuído localmente em `seed/references/guide-instagram.md` e com ligação para a [fonte original no GitHub](https://github.com/gyoridavid/ai_agents_az/blob/main/episode_8/guide-instagram.md). A página **Configurações > Notificações** mantém um histórico persistente de conclusões e falhas, reconcilia estados escritos por componentes locais e disponibiliza um checkbox independente para cada operação mapeada.
|
|
75
|
+
A página **AI Influencers > Tutorial Meta** apresenta o guia de configuração de uma conta Instagram profissional e das credenciais Meta para automações com n8n, distribuído localmente em `seed/references/guide-instagram.md` e com ligação para a [fonte original no GitHub](https://github.com/gyoridavid/ai_agents_az/blob/main/episode_8/guide-instagram.md). A página **Configurações > Notificações** mantém um histórico persistente de conclusões e falhas, reconcilia estados escritos por componentes locais e disponibiliza um checkbox independente para cada operação mapeada. A subaba **Geral** mantém esse centro local; a subaba **Telegram** permite configurar Bot Token, Chat ID e proxy opcional e envia as mesmas notificações pela Bot API oficial, sem polling ou mensagens recebidas.
|
|
76
76
|
|
|
77
77
|
A página **Edição > Download Mídia** utiliza a API Python do [yt-dlp](https://github.com/yt-dlp/yt-dlp) para descarregar vídeos e áudio de URLs públicas, com qualidade, contentor, formato de áudio, legendas, metadados, playlists, progresso e histórico local em `storage/downloads/` e `storage/state/media_downloads.json`. Conversão e combinação de streams podem exigir FFmpeg.
|
|
78
78
|
|
|
@@ -193,9 +193,9 @@ thunderbolt
|
|
|
193
193
|
No Windows PowerShell, se `npx` for bloqueado por `npx.ps1`, use directamente `npx.cmd`:
|
|
194
194
|
|
|
195
195
|
```powershell
|
|
196
|
-
npx.cmd --yes @danhachuel/thunderbolt@0.
|
|
197
|
-
npx.cmd --yes @danhachuel/thunderbolt@0.
|
|
198
|
-
npx.cmd --yes @danhachuel/thunderbolt@0.
|
|
196
|
+
npx.cmd --yes @danhachuel/thunderbolt@0.3.35 install
|
|
197
|
+
npx.cmd --yes @danhachuel/thunderbolt@0.3.35 doctor
|
|
198
|
+
npx.cmd --yes @danhachuel/thunderbolt@0.3.35
|
|
199
199
|
```
|
|
200
200
|
|
|
201
201
|
Como alternativa, pode permitir scripts para o seu utilizador:
|
|
@@ -219,7 +219,7 @@ O menu expansível **Niche Finder** contém duas alternativas independentes. **N
|
|
|
219
219
|
|
|
220
220
|
A interface apresenta dentro da aba os parâmetros da busca: número de clusters entre 2 e 10, suporte mínimo entre 0,01 e 0,50, país, categoria de engagement, intervalo de datas e tags. A página permanece sem resultados até o primeiro clique em **Analisar Nichos**; depois, se os parâmetros forem alterados, mostra os resultados anteriores e pede novo clique para aplicar os filtros actuais. O núcleo aplica normalização, filtros, `log1p`, `StandardScaler`, K-Means e FP-Growth. Os resultados aparecem em DataFrames para clusters, itemsets frequentes, regras de associação e dados analisados, acompanhados por uma visualização Plotly nativa e pesquisa de palavras nos clusters.
|
|
221
221
|
|
|
222
|
-
**Niche Finder Apify** é a segunda alternativa e não usa o dataset, filtros, execução ou resultados Kaggle. Define três palavras-chave, período, limite de resultados, Shorts, duração, idioma de legendas e ordenação; depois de clicar em **Pesquisar no Apify**, inicia o actor `streamers~youtube-scraper`, acompanha o run, carrega o dataset, normaliza vídeos, limpa SRT, calcula VSC Ratio e tenta resumir as transcrições com o provider LLM configurado. Os resultados ficam na sessão própria `niche_apify_results`, o histórico pequeno fica em `storage/state/niche_apify_runs.json` e existem exportações JSON/CSV. Configure o **Apify API Token** em **Configuração API > API Keys > Serviços e modelos**. As dependências adicionais são instaladas pelo fluxo normal do pacote: `requests`, `pandas` e os componentes já existentes da análise. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.
|
|
222
|
+
**Niche Finder Apify** é a segunda alternativa e não usa o dataset, filtros, execução ou resultados Kaggle. Define três palavras-chave, período, limite de resultados, Shorts, duração, idioma de legendas e ordenação; depois de clicar em **Pesquisar no Apify**, inicia o actor `streamers~youtube-scraper`, acompanha o run, carrega o dataset, normaliza vídeos, limpa SRT, calcula VSC Ratio e tenta resumir as transcrições com o provider LLM configurado. Os resultados ficam na sessão própria `niche_apify_results`, o histórico pequeno fica em `storage/state/niche_apify_runs.json` e existem exportações JSON/CSV. Configure o **Apify API Token** em **Configuração API > API Keys > Serviços e modelos**. As dependências adicionais são instaladas pelo fluxo normal do pacote: `requests`, `pandas` e os componentes já existentes da análise. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.3.35 install`; o instalador detecta e reutiliza componentes já válidos.
|
|
223
223
|
|
|
224
224
|
## Editor Python baseado no PYEdit
|
|
225
225
|
|
package/app/main.py
CHANGED
|
@@ -42,7 +42,7 @@ from hermes_ui.music import list_music_files, materialize_suno_audio, request_su
|
|
|
42
42
|
from hermes_ui.media_downloader import AUDIO_FORMATS, VIDEO_CONTAINERS, VIDEO_QUALITY_OPTIONS, MediaDownloadError, build_download_options, clear_media_download_history, dependency_status, download_media, list_media_downloads, media_download_file
|
|
43
43
|
from hermes_ui.notifications import clear_notifications, list_notifications, mark_all_notifications_read, mark_notification_read, notification_event_catalog, notification_preferences, record_notification, reconcile_persisted_notifications, save_notification_preferences, unread_notification_count
|
|
44
44
|
from hermes_ui.languages import LANGUAGE_CODES, VIDEO_LANGUAGE_CODES, LANGUAGE_FLAG_DATA_URIS, language_code, language_label, ui_language_menu_label, ui_text, video_language_label, video_language_options
|
|
45
|
-
from hermes_ui.api_key_tests import test_apify_credentials, test_kaggle_credentials, test_material_source_credentials, test_nano_banana_credentials, test_postiz_credentials, test_tiktok_credentials, test_upload_post_credentials, test_voice_provider
|
|
45
|
+
from hermes_ui.api_key_tests import test_apify_credentials, test_kaggle_credentials, test_material_source_credentials, test_nano_banana_credentials, test_postiz_credentials, test_telegram_credentials, test_tiktok_credentials, test_upload_post_credentials, test_voice_provider
|
|
46
46
|
from hermes_ui.tutorials import tutorial_body, tutorial_caption, tutorial_title
|
|
47
47
|
|
|
48
48
|
from hermes_ui.script_documents import list_script_documents, read_script_document, save_script_document, script_storage_path
|
|
@@ -4955,93 +4955,166 @@ def render_settings():
|
|
|
4955
4955
|
st.warning("A amostra de voz anterior não é um ficheiro de áudio legível e foi removida do estado local. Teste a voz novamente.")
|
|
4956
4956
|
|
|
4957
4957
|
|
|
4958
|
+
def _render_telegram_notification_settings() -> None:
|
|
4959
|
+
"""Render Telegram outbound notification settings and a safe diagnostics action."""
|
|
4960
|
+
settings = read_json("settings.json", {})
|
|
4961
|
+
if not isinstance(settings, dict):
|
|
4962
|
+
settings = {}
|
|
4963
|
+
st.subheader("Telegram Gateway")
|
|
4964
|
+
st.caption("Envie para o Telegram as mesmas notificações que aparecem na subaba Geral. A integração usa a Bot API oficial e não recebe mensagens nem inicia polling.")
|
|
4965
|
+
with st.form("telegram_notifications_form"):
|
|
4966
|
+
telegram_enabled = st.checkbox(
|
|
4967
|
+
"Activar notificações Telegram",
|
|
4968
|
+
value=bool(settings.get("telegram_enabled", False)),
|
|
4969
|
+
help="Quando activo, cada nova notificação permitida em Geral é enviada para o Chat ID configurado.",
|
|
4970
|
+
)
|
|
4971
|
+
telegram_cols = st.columns(2)
|
|
4972
|
+
with telegram_cols[0]:
|
|
4973
|
+
telegram_bot_token = st.text_input(
|
|
4974
|
+
"Telegram Bot Token",
|
|
4975
|
+
value=str(settings.get("telegram_bot_token") or ""),
|
|
4976
|
+
type="password",
|
|
4977
|
+
help="Token criado pelo BotFather. Fica guardado apenas no storage local.",
|
|
4978
|
+
)
|
|
4979
|
+
_render_credential_status(telegram_bot_token)
|
|
4980
|
+
with telegram_cols[1]:
|
|
4981
|
+
telegram_chat_id = st.text_input(
|
|
4982
|
+
"Telegram Chat ID",
|
|
4983
|
+
value=str(settings.get("telegram_chat_id") or ""),
|
|
4984
|
+
help="ID do utilizador, grupo ou canal que receberá as notificações; também pode ser um username aceite pelo Telegram.",
|
|
4985
|
+
)
|
|
4986
|
+
telegram_proxy_url = st.text_input(
|
|
4987
|
+
"Proxy Telegram (opcional)",
|
|
4988
|
+
value=str(settings.get("telegram_proxy_url") or ""),
|
|
4989
|
+
help="Proxy HTTP/HTTPS/SOCKS suportado pelo ambiente, quando o acesso directo ao Telegram não estiver disponível.",
|
|
4990
|
+
)
|
|
4991
|
+
telegram_timeout_seconds = st.number_input(
|
|
4992
|
+
"Timeout de envio (segundos)",
|
|
4993
|
+
min_value=5,
|
|
4994
|
+
max_value=120,
|
|
4995
|
+
value=int(settings.get("telegram_timeout_seconds", 15) or 15),
|
|
4996
|
+
step=5,
|
|
4997
|
+
)
|
|
4998
|
+
st.caption("O teste consulta getMe para validar o Bot Token e não envia uma mensagem de teste.")
|
|
4999
|
+
_render_api_test_control(
|
|
5000
|
+
settings,
|
|
5001
|
+
"telegram",
|
|
5002
|
+
lambda: test_telegram_credentials(telegram_bot_token, telegram_chat_id),
|
|
5003
|
+
widget_key="api_test_telegram",
|
|
5004
|
+
)
|
|
5005
|
+
if st.form_submit_button("Guardar configuração Telegram", type="primary", use_container_width=True):
|
|
5006
|
+
settings.update({
|
|
5007
|
+
"telegram_enabled": bool(telegram_enabled),
|
|
5008
|
+
"telegram_bot_token": telegram_bot_token.strip(),
|
|
5009
|
+
"telegram_chat_id": telegram_chat_id.strip(),
|
|
5010
|
+
"telegram_proxy_url": telegram_proxy_url.strip(),
|
|
5011
|
+
"telegram_timeout_seconds": int(telegram_timeout_seconds),
|
|
5012
|
+
})
|
|
5013
|
+
write_json("settings.json", settings)
|
|
5014
|
+
st.success("Configuração Telegram guardada no storage local.")
|
|
5015
|
+
st.rerun()
|
|
5016
|
+
|
|
5017
|
+
if telegram_enabled and telegram_bot_token.strip() and telegram_chat_id.strip():
|
|
5018
|
+
st.success("Telegram está preparado para receber novas notificações.")
|
|
5019
|
+
elif telegram_enabled:
|
|
5020
|
+
st.warning("Telegram está activo, mas ainda falta configurar o Bot Token e o Chat ID.")
|
|
5021
|
+
else:
|
|
5022
|
+
st.info("Telegram está desactivado. As notificações continuam disponíveis na subaba Geral.")
|
|
5023
|
+
|
|
5024
|
+
|
|
4958
5025
|
def render_notifications():
|
|
4959
5026
|
st.title("Notificações")
|
|
4960
5027
|
st.caption("Centro de notificações internas persistentes do Thunderbolt. As conclusões são guardadas no storage local e aparecem quando a aplicação é actualizada.")
|
|
4961
|
-
|
|
4962
|
-
preferences = notification_preferences()
|
|
4963
|
-
catalog = notification_event_catalog()
|
|
4964
|
-
notifications = list_notifications(limit=500)
|
|
4965
|
-
unread_count = unread_notification_count()
|
|
4966
|
-
summary_cols = st.columns(3)
|
|
4967
|
-
with summary_cols[0]:
|
|
4968
|
-
st.metric("Não lidas", unread_count)
|
|
4969
|
-
with summary_cols[1]:
|
|
4970
|
-
st.metric("Total guardado", len(notifications))
|
|
4971
|
-
with summary_cols[2]:
|
|
4972
|
-
st.metric("Operações mapeadas", len(catalog))
|
|
4973
|
-
|
|
4974
|
-
action_cols = st.columns([1.4, 1.4, 2.2])
|
|
4975
|
-
with action_cols[0]:
|
|
4976
|
-
if st.button("Marcar todas como lidas", use_container_width=True, disabled=unread_count == 0):
|
|
4977
|
-
mark_all_notifications_read()
|
|
4978
|
-
st.rerun()
|
|
4979
|
-
with action_cols[1]:
|
|
4980
|
-
if st.button("Actualizar notificações", use_container_width=True):
|
|
4981
|
-
reconcile_persisted_notifications()
|
|
4982
|
-
st.rerun()
|
|
4983
|
-
with action_cols[2]:
|
|
4984
|
-
confirm_clear = st.checkbox("Confirmar limpeza do histórico", key="confirm_clear_notifications")
|
|
4985
|
-
if st.button("Limpar histórico", use_container_width=True, disabled=not confirm_clear):
|
|
4986
|
-
clear_notifications()
|
|
4987
|
-
st.session_state.pop("confirm_clear_notifications", None)
|
|
4988
|
-
st.rerun()
|
|
5028
|
+
general_tab, telegram_tab = render_localized_tabs(["Geral", "Telegram"])
|
|
4989
5029
|
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
st.
|
|
5030
|
+
with general_tab:
|
|
5031
|
+
reconcile_persisted_notifications()
|
|
5032
|
+
preferences = notification_preferences()
|
|
5033
|
+
catalog = notification_event_catalog()
|
|
5034
|
+
notifications = list_notifications(limit=500)
|
|
5035
|
+
unread_count = unread_notification_count()
|
|
5036
|
+
summary_cols = st.columns(3)
|
|
5037
|
+
with summary_cols[0]:
|
|
5038
|
+
st.metric("Não lidas", unread_count)
|
|
5039
|
+
with summary_cols[1]:
|
|
5040
|
+
st.metric("Total guardado", len(notifications))
|
|
5041
|
+
with summary_cols[2]:
|
|
5042
|
+
st.metric("Operações mapeadas", len(catalog))
|
|
5043
|
+
|
|
5044
|
+
action_cols = st.columns([1.4, 1.4, 2.2])
|
|
5045
|
+
with action_cols[0]:
|
|
5046
|
+
if st.button("Marcar todas como lidas", use_container_width=True, disabled=unread_count == 0):
|
|
5047
|
+
mark_all_notifications_read()
|
|
5048
|
+
st.rerun()
|
|
5049
|
+
with action_cols[1]:
|
|
5050
|
+
if st.button("Actualizar notificações", use_container_width=True):
|
|
5051
|
+
reconcile_persisted_notifications()
|
|
5052
|
+
st.rerun()
|
|
5053
|
+
with action_cols[2]:
|
|
5054
|
+
confirm_clear = st.checkbox("Confirmar limpeza do histórico", key="confirm_clear_notifications")
|
|
5055
|
+
if st.button("Limpar histórico", use_container_width=True, disabled=not confirm_clear):
|
|
5056
|
+
clear_notifications()
|
|
5057
|
+
st.session_state.pop("confirm_clear_notifications", None)
|
|
5058
|
+
st.rerun()
|
|
5011
5059
|
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
st.
|
|
5032
|
-
st.
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5060
|
+
st.divider()
|
|
5061
|
+
st.subheader("Operações notificadas")
|
|
5062
|
+
st.caption("Ligue ou desligue cada tipo de notificação. As preferências ficam guardadas no storage local e aplicam-se às próximas conclusões.")
|
|
5063
|
+
grouped: dict[str, list[dict[str, str]]] = {}
|
|
5064
|
+
for event in catalog:
|
|
5065
|
+
grouped.setdefault(event["category"], []).append(event)
|
|
5066
|
+
with st.form("notification_preferences_form"):
|
|
5067
|
+
pending_preferences: dict[str, bool] = {}
|
|
5068
|
+
for category, events in grouped.items():
|
|
5069
|
+
st.markdown(f"**{category}**")
|
|
5070
|
+
for event in events:
|
|
5071
|
+
pending_preferences[event["code"]] = st.checkbox(
|
|
5072
|
+
event["label"],
|
|
5073
|
+
value=bool(preferences.get(event["code"], True)),
|
|
5074
|
+
help=event["description"],
|
|
5075
|
+
key=f"notification_preference_{event['code']}",
|
|
5076
|
+
)
|
|
5077
|
+
if st.form_submit_button("Guardar preferências", type="primary", use_container_width=True):
|
|
5078
|
+
save_notification_preferences(pending_preferences)
|
|
5079
|
+
st.success("Preferências de notificação guardadas.")
|
|
5080
|
+
st.rerun()
|
|
5081
|
+
|
|
5082
|
+
st.divider()
|
|
5083
|
+
st.subheader("Histórico de notificações")
|
|
5084
|
+
filter_cols = st.columns([1, 1.4])
|
|
5085
|
+
category_options = ["Todas"] + sorted({event["category"] for event in catalog})
|
|
5086
|
+
with filter_cols[0]:
|
|
5087
|
+
selected_category = st.selectbox("Categoria", category_options, key="notifications_category_filter")
|
|
5088
|
+
with filter_cols[1]:
|
|
5089
|
+
selected_state = st.selectbox("Estado", ["Todas", "Não lidas", "Lidas"], key="notifications_state_filter")
|
|
5090
|
+
category_filter = "" if selected_category == "Todas" else selected_category
|
|
5091
|
+
unread_filter = selected_state == "Não lidas"
|
|
5092
|
+
filtered = list_notifications(limit=500, category=category_filter, unread_only=unread_filter)
|
|
5093
|
+
if selected_state == "Lidas":
|
|
5094
|
+
filtered = [item for item in filtered if item.get("read")]
|
|
5095
|
+
if not filtered:
|
|
5096
|
+
st.info("Ainda não existem notificações para os filtros seleccionados.")
|
|
5097
|
+
for item in filtered:
|
|
5098
|
+
with st.container(border=True):
|
|
5099
|
+
notification_cols = st.columns([3.3, 1.4, 1])
|
|
5100
|
+
with notification_cols[0]:
|
|
5101
|
+
st.write(f"**{item.get('title') or item.get('label') or 'Notificação'}**")
|
|
5102
|
+
st.caption(item.get("message") or "")
|
|
5103
|
+
metadata = item.get("metadata") if isinstance(item.get("metadata"), dict) else {}
|
|
5104
|
+
if metadata:
|
|
5105
|
+
public_details = " · ".join(f"{key}: {value}" for key, value in metadata.items() if value not in (None, ""))
|
|
5106
|
+
if public_details:
|
|
5107
|
+
st.caption(public_details)
|
|
5108
|
+
with notification_cols[1]:
|
|
5109
|
+
st.caption(f"{item.get('category', 'Sistema')} · {item.get('created_at', '—')}")
|
|
5110
|
+
st.write("Lida" if item.get("read") else "Não lida")
|
|
5111
|
+
with notification_cols[2]:
|
|
5112
|
+
if not item.get("read") and st.button("Marcar como lida", key=f"mark_notification_{item.get('id')}", use_container_width=True):
|
|
5113
|
+
mark_notification_read(str(item.get("id")))
|
|
5114
|
+
st.rerun()
|
|
5115
|
+
|
|
5116
|
+
with telegram_tab:
|
|
5117
|
+
_render_telegram_notification_settings()
|
|
5045
5118
|
|
|
5046
5119
|
|
|
5047
5120
|
def render_models_ai_tutorial():
|
|
@@ -213,6 +213,18 @@ def test_postiz_credentials(api_key: str, base_url: str = "https://api.postiz.co
|
|
|
213
213
|
return _get(f"{base}/integrations", headers={"Authorization": api_key})
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
def test_telegram_credentials(bot_token: str, chat_id: str) -> dict[str, Any]:
|
|
217
|
+
"""Validate the Telegram bot token with getMe without sending a message."""
|
|
218
|
+
bot_token = str(bot_token or "").strip()
|
|
219
|
+
chat_id = str(chat_id or "").strip()
|
|
220
|
+
if not bot_token or not chat_id:
|
|
221
|
+
return _missing("Introduza o Bot Token e o Chat ID Telegram antes de testar.")
|
|
222
|
+
result = _get(f"https://api.telegram.org/bot{bot_token}/getMe")
|
|
223
|
+
if result.get("ok"):
|
|
224
|
+
result["message"] = "Bot Token Telegram válido. O Chat ID foi aceite localmente; o teste não enviou mensagem."
|
|
225
|
+
return result
|
|
226
|
+
|
|
227
|
+
|
|
216
228
|
def test_material_source_credentials(provider: str, api_key: str) -> dict[str, Any]:
|
|
217
229
|
"""Run a read-only provider-specific check for material source cards."""
|
|
218
230
|
provider = str(provider or "").strip().lower()
|
package/hermes_ui/languages.py
CHANGED
|
@@ -614,6 +614,27 @@ for _language_code, _tab_translation in TAB_TRANSLATIONS.items():
|
|
|
614
614
|
UI_TRANSLATIONS[_language_code].update(_tab_translation)
|
|
615
615
|
|
|
616
616
|
|
|
617
|
+
_NOTIFICATION_TAB_TRANSLATIONS: dict[str, dict[str, str]] = {
|
|
618
|
+
"pt": {"Geral": "Geral", "Telegram": "Telegram"},
|
|
619
|
+
"en": {"Geral": "General", "Telegram": "Telegram"},
|
|
620
|
+
"zh": {"Geral": "常规", "Telegram": "Telegram"},
|
|
621
|
+
"de": {"Geral": "Allgemein", "Telegram": "Telegram"},
|
|
622
|
+
"vi": {"Geral": "Chung", "Telegram": "Telegram"},
|
|
623
|
+
"tr": {"Geral": "Genel", "Telegram": "Telegram"},
|
|
624
|
+
"ru": {"Geral": "Общие", "Telegram": "Telegram"},
|
|
625
|
+
"es": {"Geral": "General", "Telegram": "Telegram"},
|
|
626
|
+
"id": {"Geral": "Umum", "Telegram": "Telegram"},
|
|
627
|
+
"it": {"Geral": "Generale", "Telegram": "Telegram"},
|
|
628
|
+
"pl": {"Geral": "Ogólne", "Telegram": "Telegram"},
|
|
629
|
+
"ga": {"Geral": "Ginearálta", "Telegram": "Telegram"},
|
|
630
|
+
"ar": {"Geral": "عام", "Telegram": "Telegram"},
|
|
631
|
+
"he": {"Geral": "כללי", "Telegram": "Telegram"},
|
|
632
|
+
}
|
|
633
|
+
for _language_code, _notification_tab_translation in _NOTIFICATION_TAB_TRANSLATIONS.items():
|
|
634
|
+
if _language_code in UI_TRANSLATIONS:
|
|
635
|
+
UI_TRANSLATIONS[_language_code].update(_notification_tab_translation)
|
|
636
|
+
|
|
637
|
+
|
|
617
638
|
TUTORIAL_TRANSLATIONS: dict[str, dict[str, str]] = {
|
|
618
639
|
"pt": {"Tutorial Supabase": "Tutorial Supabase", "Guia de configuração do Supabase para automações com n8n.": "Guia de configuração do Supabase para automações com n8n."},
|
|
619
640
|
"en": {"Tutorial Supabase": "Supabase Tutorial", "Guia de configuração do Supabase para automações com n8n.": "Supabase setup guide for n8n automations."},
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
import re
|
|
6
7
|
import uuid
|
|
7
8
|
from datetime import datetime, timezone
|
|
@@ -40,6 +41,8 @@ EVENT_CATALOG: tuple[dict[str, str], ...] = (
|
|
|
40
41
|
{"code": "mcp_operation_completed", "category": "Integrações", "label": "Operação MCP concluída", "description": "Quando uma operação mutável de integração MCP terminar com sucesso."},
|
|
41
42
|
)
|
|
42
43
|
EVENTS_BY_CODE = {item["code"]: item for item in EVENT_CATALOG}
|
|
44
|
+
LOGGER = logging.getLogger(__name__)
|
|
45
|
+
|
|
43
46
|
SENSITIVE_MARKERS = (
|
|
44
47
|
"token",
|
|
45
48
|
"secret",
|
|
@@ -119,6 +122,24 @@ def _history() -> list[dict[str, Any]]:
|
|
|
119
122
|
return [item for item in saved if isinstance(item, dict)]
|
|
120
123
|
|
|
121
124
|
|
|
125
|
+
def _dispatch_telegram_notification(entry: dict[str, Any]) -> None:
|
|
126
|
+
"""Deliver a persisted event to Telegram without affecting local flows."""
|
|
127
|
+
try:
|
|
128
|
+
settings = storage.read_json("settings.json", {})
|
|
129
|
+
if not isinstance(settings, dict) or not bool(settings.get("telegram_enabled", False)):
|
|
130
|
+
return
|
|
131
|
+
from integrations.telegram_gateway import send_notification_to_telegram
|
|
132
|
+
|
|
133
|
+
result = send_notification_to_telegram(entry, settings)
|
|
134
|
+
if not result.ok:
|
|
135
|
+
error_type = result.data.get("error_type") if isinstance(result.data, dict) else ""
|
|
136
|
+
if error_type != "missing_configuration":
|
|
137
|
+
LOGGER.warning("Telegram notification delivery failed (%s).", error_type or "unknown_error")
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
# External notification delivery must never break production, uploads or UI.
|
|
140
|
+
LOGGER.warning("Telegram notification delivery failed (%s).", type(exc).__name__)
|
|
141
|
+
|
|
142
|
+
|
|
122
143
|
def record_notification(
|
|
123
144
|
event_type: str,
|
|
124
145
|
title: str,
|
|
@@ -151,6 +172,7 @@ def record_notification(
|
|
|
151
172
|
"dedupe_key": str(dedupe_key or ""),
|
|
152
173
|
}
|
|
153
174
|
storage.write_json(NOTIFICATIONS_FILE, [entry, *history][:MAX_NOTIFICATIONS])
|
|
175
|
+
_dispatch_telegram_notification(entry)
|
|
154
176
|
return entry
|
|
155
177
|
|
|
156
178
|
|
package/hermes_ui/storage.py
CHANGED
|
@@ -96,6 +96,11 @@ DEFAULTS: dict[str, Any] = {
|
|
|
96
96
|
"youtube_client_secret": "",
|
|
97
97
|
"youtube_batch_accounts": [],
|
|
98
98
|
"youtube_batch_selected_account_id": "",
|
|
99
|
+
"telegram_enabled": False,
|
|
100
|
+
"telegram_bot_token": "",
|
|
101
|
+
"telegram_chat_id": "",
|
|
102
|
+
"telegram_proxy_url": "",
|
|
103
|
+
"telegram_timeout_seconds": 15,
|
|
99
104
|
"notification_preferences": {
|
|
100
105
|
"video_completed": True,
|
|
101
106
|
"music_completed": True,
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Outbound Telegram Gateway integration for persisted Thunderbolt notifications."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from .platforms import IntegrationResult
|
|
11
|
+
|
|
12
|
+
DEFAULT_TELEGRAM_API_BASE_URL = "https://api.telegram.org"
|
|
13
|
+
TELEGRAM_MAX_MESSAGE_LENGTH = 4096
|
|
14
|
+
DEFAULT_TELEGRAM_TIMEOUT = 15
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TelegramGatewayAdapter:
|
|
18
|
+
"""Send Thunderbolt notifications through the Telegram Bot API.
|
|
19
|
+
|
|
20
|
+
The adapter deliberately implements outbound delivery only. It does not
|
|
21
|
+
poll Telegram or receive messages, which keeps notification delivery
|
|
22
|
+
isolated from the production and upload flows.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, settings: dict[str, Any] | None = None, *, request_client: Any = requests) -> None:
|
|
26
|
+
self.settings = settings or {}
|
|
27
|
+
self.enabled = bool(self.settings.get("telegram_enabled", False))
|
|
28
|
+
self.bot_token = str(self.settings.get("telegram_bot_token") or os.getenv("TELEGRAM_BOT_TOKEN", "")).strip()
|
|
29
|
+
self.chat_id = str(self.settings.get("telegram_chat_id") or os.getenv("TELEGRAM_CHAT_ID", "")).strip()
|
|
30
|
+
self.proxy_url = str(self.settings.get("telegram_proxy_url") or os.getenv("TELEGRAM_PROXY_URL", "")).strip()
|
|
31
|
+
self.timeout = max(1, int(self.settings.get("telegram_timeout_seconds", DEFAULT_TELEGRAM_TIMEOUT) or DEFAULT_TELEGRAM_TIMEOUT))
|
|
32
|
+
self._request_client = request_client
|
|
33
|
+
|
|
34
|
+
def _error(self, message: str, *, status_code: int | None = None, error_type: str = "") -> IntegrationResult:
|
|
35
|
+
data: dict[str, Any] = {}
|
|
36
|
+
if status_code is not None:
|
|
37
|
+
data["status_code"] = status_code
|
|
38
|
+
if error_type:
|
|
39
|
+
data["error_type"] = error_type
|
|
40
|
+
return IntegrationResult(False, message, data)
|
|
41
|
+
|
|
42
|
+
def status(self) -> IntegrationResult:
|
|
43
|
+
if not self.enabled:
|
|
44
|
+
return self._error("Telegram está desactivado nas notificações.", error_type="disabled")
|
|
45
|
+
if not self.bot_token or not self.chat_id:
|
|
46
|
+
return self._error("Telegram não está configurado: indique o Bot Token e o Chat ID.", error_type="missing_configuration")
|
|
47
|
+
return IntegrationResult(True, "Telegram configurado para notificações.", {"chat_id_configured": True})
|
|
48
|
+
|
|
49
|
+
def _url(self, method: str) -> str:
|
|
50
|
+
return f"{DEFAULT_TELEGRAM_API_BASE_URL}/bot{self.bot_token}/{method}"
|
|
51
|
+
|
|
52
|
+
def _request_kwargs(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
53
|
+
kwargs: dict[str, Any] = {"json": payload, "timeout": self.timeout}
|
|
54
|
+
if self.proxy_url:
|
|
55
|
+
kwargs["proxies"] = {"http": self.proxy_url, "https": self.proxy_url}
|
|
56
|
+
return kwargs
|
|
57
|
+
|
|
58
|
+
def _post(self, method: str, payload: dict[str, Any]) -> IntegrationResult:
|
|
59
|
+
try:
|
|
60
|
+
response = self._request_client.post(self._url(method), **self._request_kwargs(payload))
|
|
61
|
+
except requests.RequestException as exc:
|
|
62
|
+
return self._error("Não foi possível contactar o Telegram.", error_type=type(exc).__name__)
|
|
63
|
+
except Exception as exc:
|
|
64
|
+
return self._error("A chamada ao Telegram falhou.", error_type=type(exc).__name__)
|
|
65
|
+
|
|
66
|
+
status_code = int(getattr(response, "status_code", 0) or 0)
|
|
67
|
+
if status_code == 429:
|
|
68
|
+
return self._error("O Telegram limitou a chamada de notificação; tente novamente mais tarde.", status_code=status_code, error_type="rate_limited")
|
|
69
|
+
if status_code in {401, 403}:
|
|
70
|
+
return self._error("O Telegram rejeitou o Bot Token ou não permite o envio para este Chat ID.", status_code=status_code, error_type="unauthorized")
|
|
71
|
+
if status_code < 200 or status_code >= 300:
|
|
72
|
+
return self._error("O Telegram rejeitou a notificação.", status_code=status_code or None, error_type="http_error")
|
|
73
|
+
try:
|
|
74
|
+
payload_response = response.json()
|
|
75
|
+
except (TypeError, ValueError):
|
|
76
|
+
return self._error("O Telegram devolveu uma resposta inválida.", status_code=status_code, error_type="invalid_json")
|
|
77
|
+
if not isinstance(payload_response, dict) or payload_response.get("ok") is not True:
|
|
78
|
+
return self._error("O Telegram não aceitou a notificação.", status_code=status_code, error_type="api_error")
|
|
79
|
+
return IntegrationResult(True, "Notificação enviada pelo Telegram.", {"status_code": status_code, "result": payload_response.get("result")})
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def split_message(text: str, *, max_length: int = TELEGRAM_MAX_MESSAGE_LENGTH) -> list[str]:
|
|
83
|
+
"""Split a message at line boundaries while respecting Telegram's limit."""
|
|
84
|
+
normalized = str(text or "").strip()
|
|
85
|
+
if not normalized:
|
|
86
|
+
return []
|
|
87
|
+
chunks: list[str] = []
|
|
88
|
+
remaining = normalized
|
|
89
|
+
while len(remaining) > max_length:
|
|
90
|
+
cut = remaining.rfind("\n", 0, max_length + 1)
|
|
91
|
+
if cut < max_length // 2:
|
|
92
|
+
cut = max_length
|
|
93
|
+
chunks.append(remaining[:cut].rstrip())
|
|
94
|
+
remaining = remaining[cut:].lstrip()
|
|
95
|
+
if remaining:
|
|
96
|
+
chunks.append(remaining)
|
|
97
|
+
return chunks
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def format_notification(notification: dict[str, Any]) -> str:
|
|
101
|
+
"""Format an already-redacted local notification for Telegram."""
|
|
102
|
+
title = str(notification.get("title") or notification.get("label") or "Notificação").strip()
|
|
103
|
+
message = str(notification.get("message") or "").strip()
|
|
104
|
+
category = str(notification.get("category") or "Sistema").strip()
|
|
105
|
+
created_at = str(notification.get("created_at") or "").strip()
|
|
106
|
+
lines = [title]
|
|
107
|
+
if message:
|
|
108
|
+
lines.append(message)
|
|
109
|
+
details: list[str] = []
|
|
110
|
+
if category:
|
|
111
|
+
details.append(f"Categoria: {category}")
|
|
112
|
+
if created_at:
|
|
113
|
+
details.append(f"Data: {created_at}")
|
|
114
|
+
metadata = notification.get("metadata")
|
|
115
|
+
if isinstance(metadata, dict):
|
|
116
|
+
for key, value in metadata.items():
|
|
117
|
+
if value in (None, "", [], {}):
|
|
118
|
+
continue
|
|
119
|
+
details.append(f"{key}: {value}")
|
|
120
|
+
if details:
|
|
121
|
+
lines.extend(["", *details])
|
|
122
|
+
return "\n".join(lines)
|
|
123
|
+
|
|
124
|
+
def send_message(self, text: str) -> IntegrationResult:
|
|
125
|
+
"""Send plain text to the configured chat, splitting oversized messages."""
|
|
126
|
+
status = self.status()
|
|
127
|
+
if not status.ok:
|
|
128
|
+
return status
|
|
129
|
+
chunks = self.split_message(text)
|
|
130
|
+
if not chunks:
|
|
131
|
+
return self._error("A notificação Telegram está vazia.", error_type="empty_message")
|
|
132
|
+
message_ids: list[str] = []
|
|
133
|
+
for chunk in chunks:
|
|
134
|
+
result = self._post("sendMessage", {"chat_id": self.chat_id, "text": chunk})
|
|
135
|
+
if not result.ok:
|
|
136
|
+
return result
|
|
137
|
+
message_result = result.data.get("result") if isinstance(result.data, dict) else None
|
|
138
|
+
if isinstance(message_result, dict) and message_result.get("message_id") is not None:
|
|
139
|
+
message_ids.append(str(message_result["message_id"]))
|
|
140
|
+
return IntegrationResult(True, "Notificação enviada pelo Telegram.", {"message_ids": message_ids, "chunks": len(chunks)})
|
|
141
|
+
|
|
142
|
+
def send_notification(self, notification: dict[str, Any]) -> IntegrationResult:
|
|
143
|
+
"""Format and send one persisted Thunderbolt notification."""
|
|
144
|
+
return self.send_message(self.format_notification(notification))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def send_notification_to_telegram(notification: dict[str, Any], settings: dict[str, Any] | None = None) -> IntegrationResult:
|
|
148
|
+
"""Convenience function used by the notification persistence layer."""
|
|
149
|
+
return TelegramGatewayAdapter(settings).send_notification(notification)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
__all__ = [
|
|
153
|
+
"DEFAULT_TELEGRAM_API_BASE_URL",
|
|
154
|
+
"DEFAULT_TELEGRAM_TIMEOUT",
|
|
155
|
+
"TELEGRAM_MAX_MESSAGE_LENGTH",
|
|
156
|
+
"TelegramGatewayAdapter",
|
|
157
|
+
"send_notification_to_telegram",
|
|
158
|
+
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danhachuel/thunderbolt",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.35",
|
|
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",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
".streamlit/config.toml"
|
|
31
31
|
],
|
|
32
32
|
"scripts": {
|
|
33
|
+
"postinstall": "node scripts/npx-cache-compat.mjs",
|
|
33
34
|
"start": "node scripts/cli.mjs",
|
|
34
35
|
"check": "node scripts/cli.mjs --check",
|
|
35
36
|
"pack:check": "npm pack --dry-run"
|
package/requirements.txt
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
+
const npxCacheRoot = resolve(packageRoot, "../../..");
|
|
7
|
+
const pathParts = npxCacheRoot.split(/[\\/]+/).filter(Boolean).map((part) => part.toLowerCase());
|
|
8
|
+
const isNpxCache = pathParts.includes("_npx");
|
|
9
|
+
const packageJsonPath = resolve(npxCacheRoot, "package.json");
|
|
10
|
+
|
|
11
|
+
if (isNpxCache && !existsSync(packageJsonPath)) {
|
|
12
|
+
mkdirSync(npxCacheRoot, { recursive: true });
|
|
13
|
+
writeFileSync(packageJsonPath, JSON.stringify({ private: true }, null, 2) + "\n", "utf8");
|
|
14
|
+
}
|
|
15
|
+
|