名称: bots
描述: >-
适用于构建 Towns Protocol 机器人 - 涵盖 SDK 初始化、斜杠命令、消息处理器、反应、交互式表单、区块链操作和部署。
触发词:"towns bot"、"makeTownsBot"、"onSlashCommand"、"onMessage"、"sendInteractionRequest"、
"webhook"、"bot deployment"、"@towns-protocol/bot"
许可证: MIT
compatibility: 需要 Bun 运行时、Base 网络 RPC 访问权限、@towns-protocol/bot SDK
元数据:
author: towns-protocol
version: "2.0.0"
必须遵守以下规则,违规将导致静默失败:
0x... 格式,切勿使用用户名。<@{userId}> 格式,并且在选项的 mentions 数组中包含该用户 ID。bot.viem.account.address = 燃料钱包(签名并支付燃料费)- 必须存入 Base ETHbot.appAddress = 金库钱包(可选,用于转账)onMessage - 它们有独立的处理器。type 属性 - 而不是 case(例如:type: 'form')。txHash - 在授予访问权限前,请验证 receipt.status === 'success'。import { makeTownsBot, getSmartAccountFromUserId } from '@towns-protocol/bot'
import type { BotCommand, BotHandler } from '@towns-protocol/bot'
import { Permission } from '@towns-protocol/web3'
import { parseEther, formatEther, erc20Abi, zeroAddress } from 'viem'
import { readContract, waitForTransactionReceipt } from 'viem/actions'
import { execute } from 'viem/experimental/erc7821'
| 方法 | 签名 | 说明 |
|---|---|---|
sendMessage |
(channelId, text, opts?) → { eventId } |
opts: { threadId?, replyId?, mentions?, attachments? } |
editMessage |
(channelId, eventId, text) |
仅限机器人自己的消息 |
removeEvent |
(channelId, eventId) |
仅限机器人自己的消息 |
sendReaction |
(channelId, messageId, emoji) |
|
sendInteractionRequest |
(channelId, payload) |
表单、交易、签名 |
hasAdminPermission |
(userId, spaceId) → boolean |
|
ban / unban |
(userId, spaceId) |
需要 ModifyBanning 权限 |
| 属性 | 描述 |
|---|---|
bot.viem |
用于区块链交互的 Viem 客户端 |
bot.viem.account.address |
燃料钱包 - 必须存入 Base ETH |
bot.appAddress |
金库钱包(可选) |
bot.botId |
机器人标识符 |
详细指南请参阅 references/:
- 消息 API - 提及、线程、附件、格式化
- 区块链操作 - 读写合约、验证交易
- 交互式组件 - 表单、交易请求
- 部署 - 本地开发、Render、隧道
- 调试 - 故障排除指南
bunx towns-bot init my-bot
cd my-bot
bun install
APP_PRIVATE_DATA=<base64_credentials> # 来自 app.towns.com/developer
JWT_SECRET=<webhook_secret> # 至少 32 个字符
PORT=3000
BASE_RPC_URL=https://base-mainnet.g.alchemy.com/v2/KEY # 推荐
import { makeTownsBot } from '@towns-protocol/bot'
import type { BotCommand } from '@towns-protocol/bot'
const commands = [
{ name: 'help', description: '显示帮助' },
{ name: 'ping', description: '检查是否存活' }
] as const satisfies BotCommand[]
const bot = await makeTownsBot(
process.env.APP_PRIVATE_DATA!,
process.env.JWT_SECRET!,
{ commands }
)
bot.onSlashCommand('ping', async (handler, event) => {
const latency = Date.now() - event.createdAt.getTime()
await handler.sendMessage(event.channelId, 'Pong!延迟 ' + latency + 'ms')
})
export default bot.start()
import { z } from 'zod'
const EnvSchema = z.object({
APP_PRIVATE_DATA: z.string().min(1),
JWT_SECRET: z.string().min(32),
DATABASE_URL: z.string().url().optional()
})
const env = EnvSchema.safeParse(process.env)
if (!env.success) {
console.error('配置无效:', env.error.issues)
process.exit(1)
}
在常规消息(非斜杠命令)上触发。
bot.onMessage(async (handler, event) => {
// event: { userId, spaceId, channelId, eventId, message, isMentioned, threadId?, replyId? }
if (event.isMentioned) {
await handler.sendMessage(event.channelId, '你提到了我!')
}
})
在 /command 上触发。不会触发 onMessage。
bot.onSlashCommand('weather', async (handler, { args, channelId }) => {
// /weather San Francisco → args: ['San', 'Francisco']
const location = args.join(' ')
if (!location) {
await handler.sendMessage(channelId, '用法:/weather <地点>')
return
}
// ... 获取天气数据
})
bot.onReaction(async (handler, event) => {
// event: { reaction, messageId, channelId }
if (event.reaction === '👋') {
await handler.sendMessage(event.channelId, '我看到你挥手了!')
}
})
需要在开发者门户中启用“所有消息”模式。
bot.onTip(async (handler, event) => {
// event: { senderAddress, receiverAddress, amount (bigint), currency }
if (event.receiverAddress === bot.appAddress) {
await handler.sendMessage(event.channelId,
'感谢你打赏 ' + formatEther(event.amount) + ' ETH!')
}
})
bot.onInteractionResponse(async (handler, event) => {
switch (event.response.payload.content?.case) {
case 'form':
const form = event.response.payload.content.value
for (const c of form.components) {
if (c.component.case === 'button' && c.id === 'yes') {
await handler.sendMessage(event.channelId, '你点击了“是”!')
}
}
break
case 'transaction':
const tx = event.response.payload.content.value
if (tx.txHash) {
// **重要**:在授予访问权限前,先在链上验证
// 完整验证模式请参阅 references/BLOCKCHAIN.md
await handler.sendMessage(event.channelId,
'交易:https://basescan.org/tx/' + tx.txHash)
}
break
}
})
在使用前始终验证上下文:
bot.onSlashCommand('cmd', async (handler, event) => {
if (!event.spaceId || !event.channelId) {
console.error('上下文缺失:', { userId: event.userId })
return
}
// 可以安全继续
})
| 错误 | 修复方法 |
|---|---|
insufficient funds for gas |
向 bot.viem.account.address 存入 Base ETH |
| 提及未高亮显示 | 文本中同时包含 <@userId> 并且在 mentions 数组中包含该用户 ID |
| 斜杠命令不工作 | 在 makeTownsBot 的 commands 数组中添加命令 |
| 处理器未触发 | 检查开发者门户中的消息转发模式 |
writeContract 失败 |
对于外部合约,请使用 execute() |
仅凭 txHash 就授予访问权限 |
先验证 receipt.status === 'success' |
| 消息行重叠 | 使用 \n\n(双换行符),而不是 \n |
| 缺少事件上下文 | 在使用前验证 spaceId/channelId |