- TypeScript 98.5%
- Shell 1%
- Dockerfile 0.5%
|
Anderson da Campo
c490f1ad8f
All checks were successful
deploy / deploy (push) Successful in 19s
AgentService: detect first/last/count intents in pt-BR and answer via SQL; inject structured channel context (stats, coverage, latest summary) into every prompt; pass excludeSourceId to RAG so the current user message cannot appear as a source. SummaryStoreService: channel_messages.content now nullable with content_expires_at metadata (metadata is permanent, text expires per MESSAGE_RETENTION_DAYS=7); add channel_sync_state separating latest live cursor from oldest backfilled cursor; expose getChannelStats, getFirstStoredMessage, getLastStoredMessage, getChannelSyncState, upsertChannelSyncState, getSummaryCoveringTimestamp, expireOldMessageContent; on boot, null out expired content automatically. RagStoreService: unique index on (source_type, source_id) prevents duplicates; saveDocument silently skips 23505 collisions; searchHybrid accepts excludeSourceId; cleanupExpiredDocuments removes discord_message chunks older than retention while keeping discord_summary forever; getStats now reports discordMessages vs summaries separately. DiscordService: every persistence and checkpoint call is now awaited; mentions persist -> respond -> index RAG (no more self-citation race); bot replies are no longer saved to the ledger; channel sync writes both ingest_checkpoint and channel_sync_state with the actual newest message; backfill loops over the backfill cursor and ingests recent messages into RAG; content for messages older than retention is stored as null with content_expires_at; MESSAGE_RETENTION_DAYS default aligned to 7 days. |
||
|---|---|---|
| .forgejo/workflows | fix: RAG SQL param index, catch error, unify OPENAI vars, remove embeddings | |
| scripts | refactor app | |
| src | feat(context): deterministic SQL routes, retention ledger, race fixes | |
| .containerignore | added podman scripts and Containerfile | |
| .env.example | fix: RAG SQL param index, catch error, unify OPENAI vars, remove embeddings | |
| .gitignore | first commit | |
| compose.yaml | refactor(storage): migrate SummaryStoreService from node:sqlite to pg Pool | |
| Dockerfile | fix(build): align solid-js peer to match @opentui requirement | |
| IDEAS.md | doc: added idea | |
| nest.cli.json | first commit | |
| package-lock.json | chore(deps): remove unused pgvector package | |
| package.json | chore(deps): remove unused pgvector package | |
| README.md | fix: RAG SQL param index, catch error, unify OPENAI vars, remove embeddings | |
| tsconfig.json | refactor app | |
Discord Bot de Resumo com NestJS
Bot para Discord que resume mensagens de um canal usando a API da OpenCode, guarda memoria recente em Postgres e responde tanto por slash command quanto por @mencao.
Funcionalidades
- Comando
/resumircom filtro por horas e limite de mensagens. - Atualizacao de status na mesma resposta do Discord enquanto o resumo e processado.
- Resumo incremental por canal: depois do primeiro resumo, o bot considera apenas mensagens novas.
- Persistencia em Postgres do historico de resumos, do ultimo
message_idresumido por canal e das mensagens dos ultimos 3 dias. - Sincronizacao inicial no startup para puxar pelo menos as ultimas 12 horas de mensagens dos canais acessiveis.
- Resposta por
@mencaoao bot com uso do contexto recente do canal e do ultimo resumo salvo. - Comando
/historicopara listar mensagens recentes sem chamar a IA. - Fallback local simples quando
OPENCODE_API_KEYnao estiver configurada ou a chamada externa falhar.
Stack
- Node.js 24
- NestJS 10
- discord.js 14
- Postgres com pgvector nativo via
pgepgvector/pg
Estrutura
src/
app.module.ts
discord/
discord.module.ts
discord.service.ts
storage/
storage.module.ts
summary-store.service.ts
summarizer/
summarizer.module.ts
summarizer.service.ts
data/
Variaveis de ambiente
Copie .env.example para .env e preencha os valores.
| Variavel | Obrigatoria | Descricao | Default |
|---|---|---|---|
DISCORD_BOT_TOKEN |
sim | Token do bot no Discord Developer Portal | - |
OPENAI_API_KEY |
sim | Chave da API OpenAI-compatible | - |
OPENAI_BASE_URL |
nao | Base da API OpenAI-compatible | https://omni.andersonc.dev.br/v1 |
OPENAI_MODEL |
nao | Modelo no campo model da API |
antigravity/gemini-3.5-flash-preview |
POSTGRES_HOST |
nao | Host do Postgres | localhost |
POSTGRES_PORT |
nao | Porta do Postgres | 5432 |
POSTGRES_USER |
nao | Usuario do Postgres | postgres |
POSTGRES_DB |
nao | Nome do banco Postgres | crom_rag |
MESSAGE_RETENTION_DAYS |
nao | Dias de mensagens retidas no banco | 3 |
STARTUP_SYNC_HOURS |
nao | Horas de hist. hydrate no startup | 168 |
STARTUP_SYNC_MAX_MESSAGES_PER_CHANNEL |
nao | Limite de msgs por canal no startup | 500 |
CROM_HOST |
nao | Hostname do bot (usado no resumo) | crombot.andersonc.dev.br |
Instalacao
npm install
Executar em desenvolvimento
npm run start:dev
Build e producao
npm run build
npm run start:prod
Como configurar o bot no Discord
- Crie uma application no Discord Developer Portal.
- Crie o bot e copie o token para
DISCORD_BOT_TOKEN. - Em
Bot > Privileged Gateway Intents, habiliteMessage Content Intent. - Convide o bot para o servidor com escopos
boteapplications.commands. - Inicie o projeto.
- No log, confirme que apareceram:
Bot conectado como ...
Slash commands registrados na guild <id>.
Observacao:
- O bot registra slash commands em todas as guilds carregadas no startup.
- Quando entra em uma guild nova, registra automaticamente os comandos nela.
Comandos do bot
/resumir
Resume apenas mensagens novas do canal desde o ultimo resumo salvo no SQLite.
Opcoes:
horas: janela de busca para tras. Minimo1, maximo168. Default1.mensagens: limite maximo de mensagens novas consideradas. Minimo5, maximo100. Default50.
Exemplos:
/resumir horas:1 mensagens:50
/resumir horas:24 mensagens:100
/resumir mensagens:20
Fluxo:
- O bot responde imediatamente com status.
- Busca mensagens do canal.
- Para no ultimo
message_idja resumido para aquele canal. - Monta o contexto.
- Chama a OpenCode.
- Salva resumo e checkpoint no Postgres.
- Edita a mesma mensagem com o resultado final.
Se nao houver mensagens novas desde o ultimo resumo, o bot informa isso e nao chama a IA.
/historico
Lista mensagens recentes do canal sem resumir.
Opcao:
mensagens: minimo5, maximo50, default20.
@bot ...
Quando um usuario menciona o bot em um canal, ele responde como assistente usando:
- mensagens salvas no Postgres dos ultimos 3 dias
- filtro heuristico para puxar mensagens mais relevantes para o pedido
- ultimo resumo salvo do canal, quando existir
Exemplos:
@bot me resume o que estavam falando hoje
@bot o que o Joao falou sobre deploy?
@bot me atualiza do que mudou desde ontem
Comportamento:
- O bot salva a mensagem do usuario no SQLite.
- Remove a mencao do texto e trata o resto como prompt.
- Busca contexto recente do mesmo canal.
- Tenta filtrar mensagens mais relevantes pelos termos do pedido.
- Envia contexto + ultimo resumo do canal para a OpenCode.
- Responde no proprio canal.
- Salva a resposta final dele tambem no SQLite.
Como funciona o resumo incremental
O projeto salva dois tipos de dado:
summary_checkpoints: ultimo resumo por canalsummaries: historico completo dos resumos geradoschannel_messages: memoria recente de mensagens por canal
Campos principais persistidos:
channel_idguild_idfrom_message_idto_message_idfrom_timestampto_timestampmessage_countsummary
Fluxo incremental:
- Primeira execucao no canal: o bot busca mensagens dentro da janela e do limite pedido.
- Execucoes seguintes:
o bot continua buscando do mais recente para tras, mas interrompe ao encontrar o ultimo
message_idsalvo no checkpoint.
Isso evita resumir repetidamente o mesmo conteudo para pessoas diferentes no mesmo canal.
Memoria de mensagens
O bot tambem guarda mensagens recentes em Postgres para sustentar o modo conversacional por @mencao.
Campos principais persistidos em channel_messages:
message_idguild_idchannel_idauthor_idauthor_usernameauthor_display_namecontentcreated_at
Politica atual:
- mensagens de bots sao ignoradas
- mensagens vazias sao ignoradas
- a retencao padrao e de 3 dias
- o armazenamento usa upsert por
message_id - no startup, o bot tenta hidratar pelo menos as ultimas 12 horas dos canais acessiveis
OpenCode
O resumo usa chamada HTTP direta para:
POST https://opencode.ai/zen/v1/chat/completions
Corpo relevante:
{
"model": "minimax-m2.5-free",
"max_tokens": 8192,
"messages": [
{ "role": "system", "content": "..." },
{ "role": "user", "content": "..." }
]
}
Se OPENCODE_API_KEY nao estiver definida, o app retorna um resumo simples local apenas com preview das mensagens.
Banco Postgres
Conexao atraves das variaveis POSTGRES_*. O banco crom_rag e criado automaticamente se nao existir.
Tabelas:
-- Resumos e checkpoints
SELECT*FROMsummary_checkpoints;SELECTid,channel_id,message_count,created_atFROMsummariesORDERBYidDESC;-- Mensagens recentes
SELECTchannel_id,author_display_name,created_atFROMchannel_messagesORDERBYcreated_atDESCLIMIT20;-- RAG
SELECT*FROMrag_documents;SELECT*FROMrag_chunksLIMIT10;Observacoes operacionais
- O bot ignora mensagens de outros bots.
- O bot ignora mensagens vazias no resumo incremental.
- O nome usado no contexto do resumo prioriza apelido no servidor (
displayName), depoisglobalName, depoisusername. - O status do
/resumire atualizado sempre na mesma resposta da interacao. - O modo
@mencaodepende da memoria local do SQLite e de um filtro heuristico simples por termos. - Se o canal tiver pouco contexto salvo, a resposta por mencao pode ficar mais generica.
- O bot precisa de
View ChanneleRead Message Historypara ler e hidratar canais.
Arquivos importantes
- src/discord/discord.service.ts: comandos do Discord, coleta de mensagens e progresso da interacao.
- src/summarizer/summarizer.service.ts: integracao com a OpenCode.
- src/storage/summary-store.service.ts: SQLite, checkpoints e historico.
Riscos e proximos ajustes
- O filtro de relevancia do modo
@mencaoainda e heuristico; nomes ambiguos e pedidos muito amplos ainda podem puxar contexto ruim. - O startup sync pode ficar caro em servidores com muitos canais acessiveis; por isso existe limite por canal.
- O filtro por janela de tempo ainda depende da leitura paginada recente do canal; para canais muito movimentados, o limite de mensagens continua mandando no volume maximo analisado.
- O projeto nao tem testes automatizados ainda.