跳到主要内容

教程:构建您的第一个应用

在本教程中,您将构建一个 字数统计器 小程序,它读取活动聊天历史,按角色(用户 vs 助手)统计字数,并显示统计数据。最后,您将了解如何创建、测试、打包和发布小程序。

您将学到什么:

  • 创建 manifest.json
  • 使用 window.ais SDK 编写应用 HTML
  • 使用 ais.chat.getHistory() 读取消息
  • 使用 ais.storage 跨会话持久化数据
  • 使用侧载进行本地测试
  • 打包为 .ais
  • 发布到社区注册表

所需时间: 约 15 分钟。


第一步:创建清单

每个小程序都需要一个 manifest.json,描述应用、其权限和入口点。创建一个新目录并添加此文件:

mkdir word-counter
cd word-counter

创建 manifest.json

{
"name": "word-counter",
"version": "1.0.0",
"abi": 1,
"type": "mini-program",
"title": "Word Counter",
"description": "Count words in your chat history by role",
"author": { "name": "Your Name" },
"entry": "index.html",
"base_url": "https://localhost:8080/",
"permissions": ["storage", "chat:read", "ui:toast"],
"keywords": ["statistics", "utility"]
}

关键字段:

字段原因
nameword-counter唯一标识符(小写,仅连字符)
abi1必需 —— 匹配当前平台 ABI
typemini-program告诉平台这是沙箱 iframe 应用
entryindex.html要加载的 HTML 文件
base_urlhttps://localhost:8080/资产托管位置(生产环境需更新)
permissions["storage", "chat:read", "ui:toast"]我们需要读取聊天并保存统计
信息

storage 权限始终自动授予,但明确列出它是好的做法,以便用户知道您的应用存储数据。


第二步:创建 HTML

在同一目录中创建 index.html。这是您的整个应用 —— HTML、CSS 和 JavaScript 在一个文件中。

<!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>字数统计器</h1>

<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="total-words">--</div>
<div class="stat-label">总字数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="user-words">--</div>
<div class="stat-label">您的字数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="ai-words">--</div>
<div class="stat-label">AI 字数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="msg-count">--</div>
<div class="stat-label">消息数</div>
</div>
</div>

<div class="actions">
<button class="btn-primary" id="analyze">分析聊天</button>
<button class="btn-close" id="close">关闭</button>
</div>

<div id="last-updated"></div>

<script>
// 辅助函数:统计字符串中的字数
function countWords(text) {
if (!text || typeof text !== "string") return 0;
return text.trim().split(/\s+/).filter(Boolean).length;
}

// 格式化数字,添加逗号 (1234 -> "1,234")
function fmt(n) {
return n.toLocaleString();
}

ais.ready(async function () {
// 设置面板标题
ais.ui.setTitle("字数统计器");

// 尝试恢复上次保存的统计
var saved = await ais.storage.get("last-stats");
if (saved) {
showStats(saved);
}

// 分析按钮
document
.getElementById("analyze")
.addEventListener("click", async function () {
this.disabled = true;
this.textContent = "分析中...";

try {
// 从聊天历史获取最多 500 条消息
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);

// 保存统计以供下次使用
await ais.storage.set("last-stats", stats);

ais.ui.toast("已分析 " + msgCount + " 条消息!");
} catch (err) {
ais.ui.toast("错误: " + err.message);
}

this.disabled = false;
this.textContent = "分析聊天";
});

// 关闭按钮
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 =
"上次分析: " + date.toLocaleString();
}
}
</script>
</body>
</html>

代码说明

  1. ais.ready() -- 在运行任何逻辑之前等待 SDK 桥接连接。
  2. ais.storage.get('last-stats') -- 恢复之前保存的统计,以便用户启动时立即看到数据。
  3. ais.chat.getHistory(500) -- 从活动对话获取最多 500 条消息。
  4. 字数统计 -- 遍历消息,按空白分割内容并按角色统计。
  5. ais.storage.set('last-stats', stats) -- 持久化结果以供下次使用。
  6. ais.ui.toast() -- 分析完成时显示通知。
  7. ais.close() -- 用户点击关闭时返回聊天视图。

第三步:本地测试

您需要本地 HTTP 服务器来提供清单和 HTML 文件。使用您喜欢的任何工具:

# Python 3
cd word-counter
python3 -m http.server 8080

# 或 Node.js
npx serve -p 8080

# 或 PHP
php -S localhost:8080

现在在平台中安装应用:

  1. 打开 aiscouncil.net 并登录
  2. 点击左侧边栏中的应用图标
  3. 侧载部分,粘贴:http://localhost:8080/manifest.json
  4. 点击安装
  5. 查看权限(storage, chat:read, ui:toast)并点击允许
  6. 点击已安装应用卡片上的打开
提示

对于最快的开发循环,使用 HTML 上传而不是 URL 侧载。直接上传您的 index.html —— 无需服务器。平台会自动创建合成清单。每次进行更改时可以卸载并重新上传。

故障排除

问题解决方案
"Failed to fetch manifest"确保本地服务器正在运行并提供 CORS 头。尝试 python3 -m http.server 8080,它提供 CORS 安全服务。
应用显示空白白页检查浏览器控制台的错误。最常见的问题是在 ais.ready() 之前调用 ais.* 方法。
"PermissionDenied: chat:read"您的清单在权限数组中不包含 chat:read。更新清单并重新安装。
代码更改后应用不更新先卸载应用(点击卡片上的 X 按钮),然后重新安装。入口 HTML 在安装时缓存。

第四步:添加一些润色

让我们添加一个功能:随着新消息到达实时统计字数。

ais.ready() 回调中关闭按钮处理程序之后添加此代码:

// 订阅新消息以进行实时计数
ais.chat.onMessage(function (msg) {
// 重新读取保存的统计并添加新消息的字数
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);
});
});

现在计数器会随着用户聊天实时更新,无需再次点击"分析"。


第五步:打包为 .ais 包

.ais 包是包含清单和所有应用文件的 ZIP 存档。平台提取 ZIP,读取清单,并将所有资产(CSS、JS、图片)内联到入口 HTML 中。

对于像我们这样的单文件应用,包很简单:

cd word-counter
zip -r ../word-counter.ais manifest.json index.html

这会在父目录中创建 word-counter.ais

测试包

  1. 在平台中,转到应用并点击上传应用
  2. 选择 word-counter.ais
  3. 查看权限并批准
  4. 应用从包中安装,所有资产已内联
信息

包是自包含的。用户不需要对原始 base_url 的网络访问 —— 所有内容在安装时内联。这使得包非常适合离线分发和分享。

多文件包

如果您的应用有单独的 CSS、JavaScript 或图片文件,将它们全部包含在 ZIP 中:

zip -r ../word-counter.ais manifest.json index.html style.css app.js icon.png

平台自动内联:

  • <link rel="stylesheet" href="style.css"> 变为 <style>...</style>
  • <script src="app.js"></script> 变为 <script>...</script>
  • <img src="icon.png"> 变为 <img src="data:image/png;base64,...">

第六步:发布到注册表

一旦您的应用准备好供他人使用,将其发布到社区注册表。

1. 托管您的文件

将清单和入口 HTML 上传到公共 CDN。GitHub Pages 免费且简单:

# 在您的 GitHub 仓库中(例如 github.com/yourname/word-counter)
# 将 manifest.json 和 index.html 推送到 main 分支
# 在仓库设置中启用 GitHub Pages(源:main 分支,根目录)

您的文件将在以下位置可用:

  • https://yourname.github.io/word-counter/manifest.json
  • https://yourname.github.io/word-counter/index.html

更新清单中的 base_url 以匹配:

"base_url": "https://yourname.github.io/word-counter/"

2. Fork aiscouncil 仓库

转到 github.com/nicholasgasior/bcz 并点击 Fork

3. 添加您的包条目

编辑 registry/packages.json 并在 packages 数组中添加条目:

{
"name": "word-counter",
"type": "mini-program",
"version": "1.0.0",
"manifest": "https://yourname.github.io/word-counter/manifest.json",
"tier": "community",
"category": "utilities",
"description": "Count words in your chat history by role",
"icon": "https://yourname.github.io/word-counter/icon.png",
"added": "2026-02-19",
"price": 0,
"currency": "USD",
"seller": null
}

4. 验证

运行验证脚本检查您的条目:

python3 registry/validate.py packages

如果验证通过,您就准备好提交了。

5. 提交拉取请求

将更改推送到您的 fork 并向主仓库创建 PR。如果自动验证通过,PR 可以合并,您的应用将出现在平台的应用商店部分。

请参阅发布到注册表了解定价、卖家设置和验证级别的完整详情。


技巧和最佳实践

设计

  • 深色主题默认 -- 大多数平台用户使用深色模式。为深色背景(#1a1a2e 或类似)设计,使用浅色文本(#e0e0e0)。
  • 48px 最小触摸目标 -- 按钮和交互元素应至少 48px 高,以便无障碍和 VLM 友好交互。
  • 14px 最小字体大小 -- 所有文本必须至少 14px 以便阅读。
  • 响应式布局 -- 应用面板宽度可变。使用 CSS Grid 或 Flexbox 配合 auto-fit 进行适配。

性能

  • 在存储中缓存结果 -- 使用 ais.storage 保存计算结果。启动时恢复它们,以便用户立即看到数据。
  • 限制聊天历史请求 -- ais.chat.getHistory(500) 通常足够。避免请求无限制历史。
  • 无轮询 -- 使用 ais.chat.onMessage() 进行实时更新,而不是重复调用 getHistory

安全

  • 请求最小权限 -- 只列出您的应用实际使用的权限。更少的权限意味着更多用户会信任并安装您的应用。
  • 验证所有输入 -- 来自 ais.chat.getHistory() 的数据包含用户生成的内容。插入 DOM 前进行清理。
  • 不要存储敏感数据 -- ais.storage 未加密。永远不要存储密码、令牌或 API 密钥(除非您的应用有 secrets:sync 并明确处理凭证传输)。

兼容性

  • 检查 ais.platform.abi -- 如果您的应用依赖特定 SDK 功能,检查 ABI 版本并在平台较旧时显示有用的消息。
  • 在 try/catch 中包装 SDK 调用 -- 权限错误和平台版本差异可能导致拒绝。优雅处理它们。
  • 使用 hello-world 示例测试 -- 平台附带一个示例小程序,您可以作为参考。