公司 1024 程序员节要搞群抽奖刚好刷到影视飓风分享的飞书内部抽奖机器人,觉得思路超实用,索性自己复刻了一个 —— 不仅能自动统计表情互动用户,还加了 AI 语义识别,说 “抽 3 人送机械键盘” 就能精准识别人数,百人抽奖全程不用动手。 www.douyin.com/video/72332… 这个是影视飓风分享的内部抽奖机器人,也是我的灵感来源
一、需求拆解:机器人核心逻辑
不用搞复杂功能,满足 “抽奖刚需” 即可,核心流程如下:
- 触发条件:群内 @机器人,且引用一条 “抽奖活动消息”(比如 “评论送书” 的消息);
- 数据采集:获取被引用消息下的所有 “表情互动用户”(避免纯文字刷屏,用表情筛选真实参与者);
- 去重 & 抽奖:对用户列表去重,按需求抽取指定人数;
- 结果公示:自动 @中奖用户,在群内发结果,不用手动艾特。
二、功能实现:3 个飞书 API + 核心代码,直接跑通
飞书开放平台提供了现成接口,不用从零写服务,重点是 “把接口串起来 + 处理边缘逻辑”(比如去重、异常捕获)。
1. 先准备 3 个核心飞书 API
这 3 个接口是基础,先在飞书开放平台申请权限(需要 “消息读取”“机器人发送消息” 权限):
| API 名称 | 作用 | 官方文档链接 |
|---|---|---|
| 接收消息事件 | 监听群内 @机器人的消息,触发抽奖逻辑 | open.feishu.cn/document/se… |
| 获取消息表情回复列表 | 拿到被引用消息下的所有表情互动用户 | open.feishu.cn/document/se… |
| 回复消息 | 发送抽奖结果,@中奖用户 | open.feishu.cn/document/se… |
2. 核心代码:从 “获取用户” 到 “随机抽奖”
下面是关键逻辑代码(用 JS 实现,其他语言可参考思路),重点看 “去重逻辑” 和 “随机抽取”,注释写得很细,新手也能懂:
/**
* 从消息表情回复中随机选用户(核心抽奖逻辑)
* @param {string} messageId - 被引用的抽奖活动消息ID
* @param {string} reactionType - 可选:指定表情类型(比如只统计👍的用户)
* @param {string} userIdType - 用户ID类型(默认user_id,飞书群内@需要这个)
* @param {number} num - 要抽取的人数(默认1人)
* @returns {Object} 抽奖结果(成功/失败、中奖用户等)
*/
async getRandomReactionUser(messageId, reactionType = null, userIdType = 'user_id', num = 1) {
try {
// 1. 调用飞书API,获取消息的所有表情回复(分页获取,避免漏数据)
const result = await this.feishuApi.getAllMessageReactions(messageId, reactionType, 50, userIdType);
// 2. 异常判断:没找到表情回复,直接返回失败
if (!result.success || result.data.length === 0) {
return {
success: false,
error: '没找到表情互动用户,可能大家还没参与~',
userIds: []
};
}
// 3. 去重:用Set存用户ID,避免同一人多次发表情重复参与
const userIdsSet = new Set();
result.data.forEach(reactions => {
// 遍历每个表情的互动用户
reactions.items?.forEach(reaction => {
// 提取用户ID(飞书返回的结构里,operator_id是用户唯一标识)
if (reaction.operator.operator_id) {
userIdsSet.add(reaction.operator.operator_id);
}
});
});
// 4. 处理“参与人数不足”的情况
const uniqueUserIds = Array.from(userIdsSet); // Set转数组,方便后续操作
if (uniqueUserIds.length === 0) {
return {
success: false,
error: '没有有效参与用户',
userIds: []
};
}
// 5. 调整抽奖人数:最多抽“参与人数”,最少抽1人
const realDrawNum = Math.max(1, Math.min(num, uniqueUserIds.length));
// 6. 随机抽奖:用“.splice”从数组中随机删元素,避免重复选
const winningUserIds = [];
const tempUserIds = [...uniqueUserIds]; // 复制数组,不修改原数据
for (let i = 0; i < realDrawNum; i++) {
// 生成随机索引(0到当前数组长度-1)
const randomIndex = Math.floor(Math.random() * tempUserIds.length);
// 从数组中删除该元素,并加入中奖列表
winningUserIds.push(tempUserIds.splice(randomIndex, 1)[0]);
}
// 7. 打印日志(方便排查问题)
logger.info(`抽奖结果:共${result.total}次表情互动,去重后${uniqueUserIds.length}人参与,抽中${realDrawNum}人:${winningUserIds.join(', ')}`);
// 8. 返回成功结果
return {
success: true,
totalReactions: result.total, // 总表情互动次数
uniqueUsers: uniqueUserIds.length, // 去重后参与人数
drawNum: realDrawNum, // 实际抽奖人数
winningUserIds: winningUserIds, // 中奖用户ID列表
allUserIds: uniqueUserIds // 所有参与用户(可选:用于公示)
};
} catch (error) {
// 异常捕获:避免机器人崩溃
logger.error('抽奖逻辑出错:', error);
return {
success: false,
error: `抽奖失败:${error.message}`,
userIds: []
};
}
}
三、扩展亮点:用 AI 语义识别,不用改代码改人数
上面基本满足了抽奖的功能,但是正常我们抽奖可能要抽的人数不一定固定为一个,那应该怎么让机器人一次正确识别应该抽几个人并且完成抽奖呢?潘天鸿在视频里没有说明我只能自己发挥,我的想法是利用AI的语义分析功能把用户的输入识别转换成JSON串,这里也可以利用飞书的aliy助手快速搭建一个识别技能。
1. 怎么实现?用飞书 Aily 助手搭技能
不用自己搭 AI 模型,飞书自带的 Aily 助手能快速做 “语义提取”,步骤超简单:
- 打开飞书 Aily 助手后台:aily.feishu.cn/;
- 新建 “语义理解技能”,设置 “触发词”(比如 “抽奖”“开奖”);
- 定义 “提取规则”:让 AI 从消息中提取 “number” 字段(比如 “抽 3 人” 提取 3,“抽 5 个” 提取 5);
- 生成 API 调用地址:参考飞书文档,用代码调用 Aily 技能(aily.feishu.cn/hc/8qluoxsa…)。
2. 代码集成:AI 识别人数后再抽奖
如此我们就只用在上面的抽奖逻辑之前加一段调用语义分析的功能就可以了:
async handleMentionMessage(eventData) {
try {
const { message } = eventData;
const { message_id, parent_id } = message;
// 1. 先获取用户@机器人时说的话(比如“@抽奖小克 抽2人”)
const messageText = this.extractMessageText(message); // 自己写个函数,提取消息纯文本
console.log('用户消息:', messageText); // 示例:“抽2人送机械键盘”
// 2. 调用飞书Aily API,提取人数
const aiResult = await this.aiService.getAilyReply(messageText);
if (!aiResult.success || !aiResult.data.number) {
// AI识别失败,默认抽1人
await this.feishuApi.replyMessage(message_id, '没识别到抽奖人数,默认抽1人哦~');
var drawNum = 1;
} else {
// AI识别成功,用提取的人数
drawNum = aiResult.data.number;
await this.feishuApi.replyMessage(message_id, `已识别:本次抽${drawNum}人,正在开奖...`);
}
// 3. 用AI识别的人数,调用抽奖函数
const drawResult = await this.getRandomReactionUser(parent_id, null, 'user_id', drawNum);
// (后续同之前:发送抽奖结果)
} catch (error) {
// 异常处理...
}
}