Tutorial: Construa Seu Primeiro App
Neste tutorial, você vai construir um mini-programa Word Counter que lê o histórico de chat ativo, conta palavras por papel (usuário vs. assistente) e exibe estatísticas. Ao final, você saberá como criar, testar, empacotar e publicar um mini-programa.
O que você aprenderá:
- Criar um
manifest.json - Escrever HTML de app com o SDK
window.ais - Usar
ais.chat.getHistory()para ler mensagens - Usar
ais.storagepara persistir dados entre sessões - Testar localmente com sideloading
- Empacotar como um bundle
.ais - Publicar no registro da comunidade
Tempo necessário: Cerca de 15 minutos.
Passo 1: Criar o Manifest
Todo mini-programa precisa de um manifest.json que descreva o app, suas permissões e seu ponto de entrada. Crie um novo diretório e adicione este arquivo:
mkdir word-counter
cd word-counter
Crie manifest.json:
{
"name": "word-counter",
"version": "1.0.0",
"abi": 1,
"type": "mini-program",
"title": "Word Counter",
"description": "Conte palavras no seu histórico de chat por papel",
"author": { "name": "Seu Nome" },
"entry": "index.html",
"base_url": "https://localhost:8080/",
"permissions": ["storage", "chat:read", "ui:toast"],
"keywords": ["statistics", "utility"]
}
Campos principais:
| Campo | Valor | Por quê |
|---|---|---|
name | word-counter | Identificador único (minúsculas, apenas hífens) |
abi | 1 | Obrigatório -- corresponde à ABI atual da plataforma |
type | mini-program | Diz à plataforma que este é um app iframe sandboxed |
entry | index.html | O arquivo HTML para carregar |
base_url | https://localhost:8080/ | Onde assets estão hospedados (atualize para produção) |
permissions | ["storage", "chat:read", "ui:toast"] | Precisamos ler chat e salvar estatísticas |
A permissão storage é sempre concedida automaticamente, mas é uma boa prática listá-la explicitamente para que usuários saibam que seu app armazena dados.
Passo 2: Criar o HTML
Crie index.html no mesmo diretório. Este é todo o seu app -- HTML, CSS e JavaScript em um arquivo.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family:
system-ui,
-apple-system,
sans-serif;
padding: 20px;
color: #e0e0e0;
background: #1a1a2e;
min-height: 100vh;
}
h1 {
font-size: 22px;
margin-bottom: 16px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.stat-card {
background: #16213e;
border: 1px solid #333;
border-radius: 8px;
padding: 16px;
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #58a6ff;
font-variant-numeric: tabular-nums;
}
.stat-label {
font-size: 14px;
color: #888;
margin-top: 4px;
}
.actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
min-height: 48px;
padding: 10px 20px;
font-size: 16px;
border: 1px solid #444;
border-radius: 6px;
background: #2a2a4e;
color: #e0e0e0;
cursor: pointer;
}
button:hover {
background: #3a3a5e;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: #1a73e8;
border-color: #1a73e8;
}
.btn-primary:hover {
background: #1557b0;
}
.btn-close {
background: none;
border-color: #666;
color: #888;
}
#last-updated {
margin-top: 16px;
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<h1>Word Counter</h1>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="total-words">--</div>
<div class="stat-label">Total de Palavras</div>
</div>
<div class="stat-card">
<div class="stat-value" id="user-words">--</div>
<div class="stat-label">Suas Palavras</div>
</div>
<div class="stat-card">
<div class="stat-value" id="ai-words">--</div>
<div class="stat-label">Palavras da IA</div>
</div>
<div class="stat-card">
<div class="stat-value" id="msg-count">--</div>
<div class="stat-label">Mensagens</div>
</div>
</div>
<div class="actions">
<button class="btn-primary" id="analyze">Analisar Chat</button>
<button class="btn-close" id="close">Fechar</button>
</div>
<div id="last-updated"></div>
<script>
// Helper: conta palavras em uma string
function countWords(text) {
if (!text || typeof text !== "string") return 0;
return text.trim().split(/\s+/).filter(Boolean).length;
}
// Formata números com vírgulas (1234 -> "1,234")
function fmt(n) {
return n.toLocaleString();
}
ais.ready(async function () {
// Define o título do painel
ais.ui.setTitle("Word Counter");
// Tenta restaurar últimas estatísticas salvas
var saved = await ais.storage.get("last-stats");
if (saved) {
showStats(saved);
}
// Botão de analisar
document
.getElementById("analyze")
.addEventListener("click", async function () {
this.disabled = true;
this.textContent = "Analisando...";
try {
// Busca até 500 mensagens do histórico de chat
var messages = await ais.chat.getHistory(500);
var userWords = 0;
var aiWords = 0;
var msgCount = messages.length;
for (var i = 0; i < messages.length; i++) {
var msg = messages[i];
var words = countWords(msg.content);
if (msg.role === "user") {
userWords += words;
} else if (msg.role === "assistant") {
aiWords += words;
}
}
var stats = {
total: userWords + aiWords,
user: userWords,
ai: aiWords,
messages: msgCount,
timestamp: Date.now(),
};
showStats(stats);
// Salva estatísticas para próxima vez
await ais.storage.set("last-stats", stats);
ais.ui.toast("Analisadas " + msgCount + " mensagens!");
} catch (err) {
ais.ui.toast("Erro: " + err.message);
}
this.disabled = false;
this.textContent = "Analisar Chat";
});
// Botão fechar
document.getElementById("close").addEventListener("click", function () {
ais.close();
});
});
function showStats(stats) {
document.getElementById("total-words").textContent = fmt(stats.total);
document.getElementById("user-words").textContent = fmt(stats.user);
document.getElementById("ai-words").textContent = fmt(stats.ai);
document.getElementById("msg-count").textContent = fmt(stats.messages);
if (stats.timestamp) {
var date = new Date(stats.timestamp);
document.getElementById("last-updated").textContent =
"Última análise: " + date.toLocaleString();
}
}
</script>
</body>
</html>
O que este código faz
ais.ready()-- Espera a bridge do SDK conectar antes de rodar qualquer lógica.ais.storage.get('last-stats')-- Restaura estatísticas salvas anteriormente para que o usuário veja dados imediatamente ao lançar.ais.chat.getHistory(500)-- Busca até 500 mensagens da conversa ativa.- Contagem de palavras -- Itera pelas mensagens, dividindo conteúdo em whitespace e somando por papel.
ais.storage.set('last-stats', stats)-- Persiste os resultados para a próxima vez.ais.ui.toast()-- Mostra uma notificação quando a análise está completa.ais.close()-- Retorna à visualização de chat quando o usuário clica Fechar.
Passo 3: Testar Localmente
Você precisa de um servidor HTTP local para servir o manifest e arquivo HTML. Use qualquer ferramenta que preferir:
# Python 3
cd word-counter
python3 -m http.server 8080
# ou Node.js
npx serve -p 8080
# ou PHP
php -S localhost:8080
Agora instale o app na plataforma:
- Abra aiscouncil.net e faça login
- Clique no ícone Apps na barra lateral esquerda
- Na seção Sideload, cole:
http://localhost:8080/manifest.json - Clique Install
- Revise as permissões (storage, chat:read, ui:toast) e clique Allow
- Clique Open no card do app instalado
Para o loop de desenvolvimento mais rápido, use HTML Upload em vez de sideloading por URL. Faça upload do seu index.html diretamente -- sem servidor necessário. A plataforma cria um manifest sintético automaticamente. Você pode desinstalar e re-uploadar cada vez que fizer mudanças.
Solução de Problemas
| Problema | Solução |
|---|---|
| "Failed to fetch manifest" | Certifique-se de que seu servidor local está rodando e servindo headers CORS. Tente python3 -m http.server 8080 que serve de forma segura para CORS. |
| App mostra página branca em branco | Verifique o console do navegador por erros. O problema mais comum é chamar métodos ais.* antes de ais.ready(). |
| "PermissionDenied: chat:read" | Seu manifest não inclui chat:read no array de permissões. Atualize o manifest e reinstale. |
| App não atualiza após mudanças de código | Desinstale o app primeiro (clique no botão X no card), depois reinstale. HTML de entrada é cacheado no momento da instalação. |
Passo 4: Adicionar Algum Polish
Vamos adicionar uma funcionalidade: contagem de palavras em tempo real conforme novas mensagens chegam.
Adicione este código dentro do callback ais.ready(), após o handler do botão fechar:
// Subscreve a novas mensagens para contagem em tempo real
ais.chat.onMessage(function (msg) {
// Re-lê estatísticas salvas e adiciona as palavras da nova mensagem
ais.storage.get("last-stats").then(function (stats) {
if (!stats) return;
var words = countWords(msg.content);
if (msg.role === "user") stats.user += words;
else if (msg.role === "assistant") stats.ai += words;
stats.total = stats.user + stats.ai;
stats.messages++;
stats.timestamp = Date.now();
showStats(stats);
ais.storage.set("last-stats", stats);
});
});
Agora os contadores atualizam ao vivo conforme o usuário conversa sem precisar clicar em "Analisar" novamente.
Passo 5: Empacotar como Bundle .ais
Um bundle .ais é um arquivo ZIP contendo seu manifest e todos os arquivos do app. A plataforma extrai o ZIP, lê o manifest e inlines todos os assets (CSS, JS, imagens) no HTML de entrada.
Para um app de arquivo único como o nosso, o bundle é simples:
cd word-counter
zip -r ../word-counter.ais manifest.json index.html
Isso cria word-counter.ais no diretório pai.
Testando o bundle
- Na plataforma, vá para Apps e clique Upload App
- Selecione
word-counter.ais - Revise permissões e aprove
- O app instala do bundle com todos os assets inlineados
Bundles são autocontidos. O usuário não precisa de acesso de rede ao base_url original -- tudo está inlineado no momento da instalação. Isso torna bundles ideais para distribuição offline e compartilhamento.
Bundles multi-arquivo
Se seu app tem arquivos CSS, JavaScript ou de imagem separados, inclua todos no ZIP:
zip -r ../word-counter.ais manifest.json index.html style.css app.js icon.png
A plataforma automaticamente inlines:
<link rel="stylesheet" href="style.css">se torna<style>...</style><script src="app.js"></script>se torna<script>...</script><img src="icon.png">se torna<img src="data:image/png;base64,...">
Passo 6: Publicar no Registro
Uma vez que seu app está pronto para outros usarem, publique-o no registro da comunidade.
1. Hospede seus arquivos
Faça upload do seu manifest e HTML de entrada para um CDN público. GitHub Pages é gratuito e fácil:
# No seu repo GitHub (ex: github.com/seunome/word-counter)
# Faça push de manifest.json e index.html para a branch main
# Habilite GitHub Pages nas configurações do repo (source: branch main, root)
Seus arquivos estarão disponíveis em:
https://seunome.github.io/word-counter/manifest.jsonhttps://seunome.github.io/word-counter/index.html
Atualize base_url no seu manifest para corresponder:
"base_url": "https://seunome.github.io/word-counter/"
2. Faça fork do repo aiscouncil
Vá para github.com/nicholasgasior/bcz e clique Fork.
3. Adicione sua entrada de pacote
Edite registry/packages.json e adicione uma entrada ao array packages:
{
"name": "word-counter",
"type": "mini-program",
"version": "1.0.0",
"manifest": "https://seunome.github.io/word-counter/manifest.json",
"tier": "community",
"category": "utilities",
"description": "Conte palavras no seu histórico de chat por papel",
"icon": "https://seunome.github.io/word-counter/icon.png",
"added": "2026-02-19",
"price": 0,
"currency": "USD",
"seller": null
}
4. Valide
Execute o script de validação para verificar sua entrada:
python3 registry/validate.py packages
Se a validação passar, você está pronto para submeter.
5. Envie um pull request
Faça push das suas mudanças para seu fork e crie um PR contra o repo principal. Se a validação automatizada passar, o PR pode ser merged e seu app aparecerá na seção App Store da plataforma.
Veja Publishing to Registry para detalhes completos sobre preços, configuração de vendedor e níveis de verificação.
Dicas e Melhores Práticas
Design
- Tema escuro padrão -- A maioria dos usuários da plataforma usa modo escuro. Design para fundos escuros (
#1a1a2eou similar) com texto claro (#e0e0e0). - Alvos de toque mínimos de 48px -- Botões e elementos interativos devem ter pelo menos 48px de altura para acessibilidade e interação amigável a VLM.
- Tamanho de fonte mínimo de 14px -- Todo texto deve ter pelo menos 14px para legibilidade.
- Layout responsivo -- A largura do painel de apps varia. Use CSS Grid ou Flexbox com
auto-fitpara adaptar.
Performance
- Cacheie resultados em armazenamento -- Use
ais.storagepara salvar resultados computados. Restaure-os ao lançar para que o usuário veja dados imediatamente. - Limite requisições de histórico de chat --
ais.chat.getHistory(500)é geralmente suficiente. Evite requisitar histórico ilimitado. - Sem polling -- Use
ais.chat.onMessage()para updates em tempo real em vez de chamargetHistoryrepetidamente.
Segurança
- Solicite permissões mínimas -- Liste apenas as permissões que seu app realmente usa. Menos permissões significa mais usuários confiarão e instalarão seu app.
- Valide toda entrada -- Dados de
ais.chat.getHistory()contêm conteúdo gerado por usuário. Sanitize antes de inserir no DOM. - Não armazene dados sensíveis --
ais.storagenão é criptografado. Nunca armazene senhas, tokens ou chaves de API (a menos que seu app tenhasecrets:synce explicitamente lide com transferência de credenciais).
Compatibilidade
- Verifique
ais.platform.abi-- Se seu app depende de funcionalidades específicas do SDK, verifique a versão da ABI e mostre uma mensagem útil se a plataforma for mais antiga. - Envolva chamadas do SDK em try/catch -- Erros de permissão e diferenças de versão da plataforma podem causar rejeições. Trate-as graciosamente.
- Teste com o exemplo hello-world -- A plataforma vem com um mini-programa de exemplo que você pode usar como referência.