手把手教你在飞书中搭建机器人

news2025/1/18 8:13:45

前言

大家好,我是潇潇雨声。飞书是一款在国内广受欢迎的企业内部管理和协同工具,同时也可以作为一个强大的个人知识管理工具。在本文中,我将帮助你迅速创建一个飞书对话机器人,并嵌入 chatGPT 的功能。这个机器人可以直接回答你的问题,也可以在群聊中被@,从而以 chatGPT 的方式提供回应。通过这样的操作,你的飞书机器人将迅速蜕变成一个支持 chatGPT 的智能助手。

一,进入飞书开发者后台进行配置

1. 首先登录到飞书的开发者后台,创建一个名为 ChatGPT 的应用

开发者后台:https://open.feishu.cn/app alt

alt

2. 保存 App ID 和 App Secret 的信息

alt

3. 为创建好的应用添加机器人能力

alt

4. 为机器人赋予权限

在权限管理页面进行权限配置,需要开通如下 6 个权限:

  • im:message
  • im:message.group_at_msg
  • im:message.group_at_msg:readonly
  • im:message.p2p_msg
  • im:message.p2p_msg:readonly
  • im:message:send_as_bot
alt

二,创建 AirCode 项目

1. 登录 AirCode,创建一个新的 Nodejs v18 的项目,项目名可以根据你的需要填写,例如填写 ChatGPT。

AirCode 网址:https://aircode.io/

alt
alt

注意:不要选上 TypeScipt

将以下的代码替换掉 hello.js 中的代码

// @version 0.0.9 调整了 axios 的报错的输出,以便于调试。
const aircode = require("aircode");
const lark = require("@larksuiteoapi/node-sdk");
var axios = require("axios");
const EventDB = aircode.db.table("event");
const MsgTable = aircode.db.table("msg"); // 用于保存历史会话的表

// 如果你不想配置环境变量,或环境变量不生效,则可以把结果填写在每一行最后的 "" 内部
const FEISHU_APP_ID = process.env.APPID || ""// 飞书的应用 ID
const FEISHU_APP_SECRET = process.env.SECRET || ""// 飞书的应用的 Secret
const FEISHU_BOTNAME = process.env.BOTNAME || ""// 飞书机器人的名字
const OPENAI_KEY = process.env.KEY || ""// OpenAI 的 Key
const OPENAI_MODEL = process.env.MODEL || "gpt-3.5-turbo"// 使用的模型
const OPENAI_MAX_TOKEN = process.env.MAX_TOKEN || 1024// 最大 token 的值

const client = new lark.Client({
  appId: FEISHU_APP_ID,
  appSecret: FEISHU_APP_SECRET,
  disableTokenCachefalse,
});

// 日志辅助函数,请贡献者使用此函数打印关键日志
function logger(param{
  console.debug(`[CF]`, param);
}

// 回复消息
async function reply(messageId, content{
  try{
    return await client.im.message.reply({
    path: {
      message_id: messageId,
    },
    data: {
      contentJSON.stringify({
        text: content,
      }),
      msg_type"text",
    },
  });
  } catch(e){
    logger("send message to feishu error",e,messageId,content);
  }
}


// 根据sessionId构造用户会话
async function buildConversation(sessionId, question{
  let prompt = [];

  // 从 MsgTable 表中取出历史记录构造 question
  const historyMsgs = await MsgTable.where({ sessionId }).find();
  for (const conversation of historyMsgs) {
      // {"role": "system", "content": "You are a helpful assistant."},
      prompt.push({"role""user""content": conversation.question})
      prompt.push({"role""assistant""content": conversation.answer})
  }

  // 拼接最新 question
  prompt.push({"role""user""content": question})
  return prompt;
}

// 保存用户会话
async function saveConversation(sessionId, question, answer{
  const msgSize =  question.length + answer.length
  const result = await MsgTable.save({
    sessionId,
    question,
    answer,
    msgSize,
  });
  if (result) {
    // 有历史会话是否需要抛弃
    await discardConversation(sessionId);
  }
}

// 如果历史会话记录大于OPENAI_MAX_TOKEN,则从第一条开始抛弃超过限制的对话
async function discardConversation(sessionId{
  let totalSize = 0;
  const countList = [];
  const historyMsgs = await MsgTable.where({ sessionId }).sort({ createdAt-1 }).find();
  const historyMsgLen = historyMsgs.length;
  for (let i = 0; i < historyMsgLen; i++) {
    const msgId = historyMsgs[i]._id;
    totalSize += historyMsgs[i].msgSize;
    countList.push({
      msgId,
      totalSize,
    });
  }
  for (const c of countList) {
    if (c.totalSize > OPENAI_MAX_TOKEN) {
      await MsgTable.where({_id: c.msgId}).delete();
    }
  }
}

// 清除历史会话
async function clearConversation(sessionId{
  return await MsgTable.where({ sessionId }).delete();
}

// 指令处理
async function cmdProcess(cmdParams{
  switch (cmdParams && cmdParams.action) {
    case "/help":
      await cmdHelp(cmdParams.messageId);
      break;
    case "/clear":
      await cmdClear(cmdParams.sessionId, cmdParams.messageId);
      break;
    default:
      await cmdHelp(cmdParams.messageId);
      break;
  }
  return { code0 }
}

// 帮助指令
async function cmdHelp(messageId{
  helpText = `ChatGPT 指令使用指南

Usage:
    /clear    清除上下文
    /help     获取更多帮助
  `

  await reply(messageId, helpText);
}

// 清除记忆指令
async function cmdClear(sessionId, messageId{
  await clearConversation(sessionId)
  await reply(messageId, "✅记忆已清除");
}

// 通过 OpenAI API 获取回复
async function getOpenAIReply(prompt{

  var data = JSON.stringify({
    model: OPENAI_MODEL,
    messages: prompt
  });

  var config = {
    method"post",
    maxBodyLengthInfinity,
    url"https://api.openai.com/v1/chat/completions",
    headers: {
      Authorization`Bearer ${OPENAI_KEY}`,
      "Content-Type""application/json",
    },
    data: data,
    timeout50000
  };

  try{
      const response = await axios(config);

      if (response.status === 429) {
        return '问题太多了,我有点眩晕,请稍后再试';
      }
      // 去除多余的换行
      return response.data.choices[0].message.content.replace("\n\n""");

  }catch(e){
     logger(e.response.data)
     return "问题太难了 出错了. (uДu〃).";
  }

}

// 自检函数
async function doctor({
  if (FEISHU_APP_ID === "") {
    return {
      code1,
      message: {
        zh_CN"你没有配置飞书应用的 AppID,请检查 & 部署后重试",
        en_US:
          "Here is no FeiSHu APP id, please check & re-Deploy & call again",
      },
    };
  }
  if (!FEISHU_APP_ID.startsWith("cli_")) {
    return {
      code1,
      message: {
        zh_CN:
          "你配置的飞书应用的 AppID 是错误的,请检查后重试。飞书应用的 APPID 以 cli_ 开头。",
        en_US:
          "Your FeiShu App ID is Wrong, Please Check and call again. FeiShu APPID must Start with cli",
      },
    };
  }
  if (FEISHU_APP_SECRET === "") {
    return {
      code1,
      message: {
        zh_CN"你没有配置飞书应用的 Secret,请检查 & 部署后重试",
        en_US:
          "Here is no FeiSHu APP Secret, please check & re-Deploy & call again",
      },
    };
  }

  if (FEISHU_BOTNAME === "") {
    return {
      code1,
      message: {
        zh_CN"你没有配置飞书应用的名称,请检查 & 部署后重试",
        en_US:
          "Here is no FeiSHu APP Name, please check & re-Deploy & call again",
      },
    };
  }

  if (OPENAI_KEY === "") {
    return {
      code1,
      message: {
        zh_CN"你没有配置 OpenAI 的 Key,请检查 & 部署后重试",
        en_US"Here is no OpenAI Key, please check & re-Deploy & call again",
      },
    };
  }

  if (!OPENAI_KEY.startsWith("sk-")) {
    return {
      code1,
      message: {
        zh_CN:
          "你配置的 OpenAI Key 是错误的,请检查后重试。OpenAI 的 KEY 以 sk- 开头。",
        en_US:
          "Your OpenAI Key is Wrong, Please Check and call again. FeiShu APPID must Start with cli",
      },
    };
  }
  return {
    code0,
    message: {
      zh_CN:
      "✅ 配置成功,接下来你可以在飞书应用当中使用机器人来完成你的工作。",
      en_US:
      "✅ Configuration is correct, you can use this bot in your FeiShu App",

    },
    meta: {
      FEISHU_APP_ID,
      OPENAI_MODEL,
      OPENAI_MAX_TOKEN,
      FEISHU_BOTNAME,
    },
  };
}

async function handleReply(userInput, sessionId, messageId, eventId{
  const question = userInput.text.replace("@_user_1""");
  logger("question: " + question);
  const action = question.trim();
  if (action.startsWith("/")) {
    return await cmdProcess({action, sessionId, messageId});
  }
  const prompt = await buildConversation(sessionId, question);
  const openaiResponse = await getOpenAIReply(prompt);
  await saveConversation(sessionId, question, openaiResponse)
  await reply(messageId, openaiResponse);

  // update content to the event record
  const evt_record = await EventDB.where({ event_id: eventId }).findOne();
  evt_record.content = userInput.text;
  await EventDB.save(evt_record);
  return { code0 };
}

module.exports = async function (params, context{
  // 如果存在 encrypt 则说明配置了 encrypt key
  if (params.encrypt) {
    logger("user enable encrypt key");
    return {
      code1,
      message: {
        zh_CN"你配置了 Encrypt Key,请关闭该功能。",
        en_US"You have open Encrypt Key Feature, please close it.",
      },
    };
  }
  // 处理飞书开放平台的服务端校验
  if (params.type === "url_verification") {
    logger("deal url_verification");
    return {
      challenge: params.challenge,
    };
  }
  // 自检查逻辑
  if (!params.hasOwnProperty("header") || context.trigger === "DEBUG") {
    logger("enter doctor");
    return await doctor();
  }
  // 处理飞书开放平台的事件回调
  if ((params.header.event_type === "im.message.receive_v1")) {
    let eventId = params.header.event_id;
    let messageId = params.event.message.message_id;
    let chatId = params.event.message.chat_id;
    let senderId = params.event.sender.sender_id.user_id;
    let sessionId = chatId + senderId;

    // 对于同一个事件,只处理一次
    const count = await EventDB.where({ event_id: eventId }).count();
    if (count != 0) {
      logger("skip repeat event");
      return { code1 };
    }
    await EventDB.save({ event_id: eventId });

    // 私聊直接回复
    if (params.event.message.chat_type === "p2p") {
      // 不是文本消息,不处理
      if (params.event.message.message_type != "text") {
        await reply(messageId, "暂不支持其他类型的提问");
        logger("skip and reply not support");
        return { code0 };
      }
      // 是文本消息,直接回复
      const userInput = JSON.parse(params.event.message.content);
      return await handleReply(userInput, sessionId, messageId, eventId);
    }

    // 群聊,需要 @ 机器人
    if (params.event.message.chat_type === "group") {
      // 这是日常群沟通,不用管
      if (
        !params.event.message.mentions ||
        params.event.message.mentions.length === 0
      ) {
        logger("not process message without mention");
        return { code0 };
      }
      // 没有 mention 机器人,则退出。
      if (params.event.message.mentions[0].name != FEISHU_BOTNAME) {
        logger("bot name not equal first mention name ");
        return { code0 };
      }
      const userInput = JSON.parse(params.event.message.content);
      return await handleReply(userInput, sessionId, messageId, eventId);
    }
  }

  logger("return without other log");
  return {
    code2,
  };
};

2. 安装 axios 以及@larksuiteoapi/node-sdk 依赖

alt

3. 配置以下四个环境变量

  • APPID:飞书的应用 ID
  • SECRET: 飞书的应用的 Secret
  • BOTNAME:飞书机器人的名字
  • KEY: OpenAI 的 Key

4. 开始部署

alt

部署成功的话,下方控制台显示内容如下

alt

4. 复制生成的链接

alt

5. 将刚刚复制的地址填充到请求地址中

alt

6. 添加接受信息的事件

alt

7. 最好填写点击版本号和更新说明以及申请理由,满足格式即可,最后点击确定,机器人就上线了。

alt

8. 打开应用

alt

9. 效果如下

alt

如果觉得我的分享对您有帮助,请关注我。创作不易,您的三连就是对我最大的支持。

alt

本文由 mdnice 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1331275.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

贪吃蛇(七)方向和屏幕刷新

由于用户玩游戏的时候&#xff0c;程序需要一边接收用户的输入&#xff0c;一边需要刷新屏幕&#xff0c;此时就需要引用线程来解决此问题。 实现思路 linux线程库pthread&#xff0c;只需要创建pthread_t 类型的线程变量&#xff0c;然后将线程变量与函数进行绑定即可&#…

HFish蜜罐搭建及简单使用

一、HFish蜜罐 HFish是一款社区型免费蜜罐&#xff0c;侧重企业安全场景&#xff0c;从内网失陷检测、外网威胁感知、威胁情报生产三个场景出发&#xff0c;为用户提供可独立操作且实用的功能&#xff0c;通过安全、敏捷、可靠的中低交互蜜罐增加用户在失陷感知和威胁情报领域的…

一篇文章带你搞定CTFMice基本操作

CTF比赛是在最短时间内拿到最多的flag&#xff0c;mice必须要有人做&#xff0c;或者一支战队必须留出一块时间专门写一些mice&#xff0c;web&#xff0c;pwn最后的一两道基本都会有难度&#xff0c;这时候就看mice的解题速度了&#xff01; 说实话&#xff0c;这是很大一块&…

JavaEE进阶学习:Spring MVC 程序开发

1.什么是 Spring MVC Spring Web MVC 是基于Servlet API 构建的原始 Web 框架&#xff0c;从一开始就包含在Spring 框架中。它的正式名称 “Spring Web MVC” 来自其源模块的名称(Spring-webmvc)&#xff0c;但它通常被称为“Spring MVC”。 从上述定义我们可以得出两个关键信…

blender径向渐变材质-着色编辑器

要点&#xff1a; 1、用纹理坐标中的物体输出连接映射中的矢量输入 2、物体选择一个空坐标&#xff0c;将空坐标延z轴上移一段距离 3、空坐标的大小要缩放到和要添加材质的物体大小保持一致

【雷达原理】雷达测速原理及实现方法

一、雷达测速原理 1.1 多普勒频率 当目标和雷达之间存在相对运动时&#xff0c;若雷达发射信号的工作频率为&#xff0c;则接收信号的频率为&#xff0c;其中为多普勒频率。将这种由于目标相对于辐射源运动而导致回波信号的频率发生变化的现象称为多普勒效应。 如图1-1所示&a…

猫头虎博客:SSH连接失败ssh: connect to host port 22: Connection refused”解决大揭秘

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

Linux bridge开启hairpin模拟测试macvlan vepa模式

看到网上介绍可以通过Linux bridge 开启hairpin方式测试macvlan vepa模式&#xff0c;但是没有找到详细资料。我尝试测试总提示错误信息&#xff0c;无法实现&#xff0c;经过几天的研究&#xff0c;我总算实现模拟测试&#xff0c;记录如下&#xff1a; 参考 1.Linux Macvla…

Angular 11到升级到 Angular 16

日新月异&#xff0c;与时俱进… 随着Angular版本不断更新&#xff0c;再看所开发的项目版本仍然是Angular 11&#xff0c;于是准备升级 截止发博日最版本是 v17.1.0&#xff0c;考虑到稳定性因素决定升级到v16版本 一&#xff1a;查看 升级指南 二&#xff1a;按照指南&…

Ubuntu 常用命令之 cal 命令用法介绍

&#x1f4d1;Linux/Ubuntu 常用命令归类整理 cal命令在Ubuntu系统下用于显示日历。它可以显示任何特定月份或整个年份的日历。 cal命令的参数如下 -1&#xff1a;只显示当前月份的日历。-3&#xff1a;显示前一个月、当前月和下一个月的日历。-s&#xff1a;指定日历的开始…

力扣思维题——寻找重复数

题目链接&#xff1a;https://leetcode.cn/problems/find-the-duplicate-number/description/?envTypestudy-plan-v2&envIdtop-100-liked 这题的思维难度较大。一种是利用双指针法进行计算环的起点&#xff0c;这种方法在面试里很难说清楚&#xff0c;也很难想到。大致做…

MyBatis中延迟加载,全局和局部的开启使用与关闭

文章目录 MyBatis中延迟加载&#xff0c;全局和局部的开启使用与关闭1、问题提出2、延迟加载和立即加载延迟加载立即加载 3、三种对应的表关系中的加载4、打开全局延迟加载&#xff08;实现一对一的延迟加载&#xff09;5、实现一对多的延迟加载&#xff08;将上面设置的全局延…

Flink 数据序列化

为 Flink 量身定制的序列化框架 大家都知道现在大数据生态非常火&#xff0c;大多数技术组件都是运行在JVM上的&#xff0c;Flink也是运行在JVM上&#xff0c;基于JVM的数据分析引擎都需要将大量的数据存储在内存中&#xff0c;这就不得不面临JVM的一些问题&#xff0c;比如Ja…

thinkphp+vue+mysql酒店客房管理系统 b1g8z

本系统包括前台界面、用户界面和管理员界面、员工界面。在前台界面里游客和用户可以浏览客房信息、公告信息等&#xff0c;用户可以预定客房&#xff0c;在用户中心界面里&#xff0c;用户可以管理预定信息&#xff0c;管理员负责用户预定的审核以及客房的发布、用户的入住等。…

装饰者模式学习

装饰器&#xff08;Decorator&#xff09;模式的定义&#xff1a;指在不改变现有对象结构的情况下&#xff0c;动态地给该对象增加一些职责&#xff08;即增加其额外功能&#xff09;的模式&#xff0c;它属于对象结构型模式。 装饰器模式的主要优点有&#xff1a; 装饰器是继…

基于Java (spring-boot)的在线考试管理系统

一、项目介绍 系统功能说明 1、系统共有管理员、老师、学生三个角色&#xff0c;管理员拥有系统最高权限。 2、老师拥有考试管理、题库管理、成绩管理、学生管理四个模块。 3、学生可以参与考试、查看成绩、试题练习、留言等功能 二、作品包含 三、项目技术 后端语言&#xff1…

【电路笔记】-串联电容器

串联电容器 文章目录 串联电容器1、概述2、示例13、示例34、总结 当电容器以菊花链方式连接在一条线上时&#xff0c;它们就串联在一起。 1、概述 对于串联电容器&#xff0c;流过电容器的充电电流 ( i C i_C iC​ ) 对于所有电容器来说都是相同的&#xff0c;因为它只有一条…

Unity中Shader缩放矩阵

文章目录 前言一、直接相乘缩放1、在属性面板定义一个四维变量&#xff0c;用xyz分别控制在xyz轴上的缩放2、在常量缓存区申明该变量3、在顶点着色器对其进行相乘&#xff0c;来缩放变换4、我们来看看效果 二、使用矩阵乘法代替直接相乘缩放的原理1、我们按如下格式得到缩放矩阵…

Koordinator 支持 K8s 与 YARN 混部,小红书在离线混部实践分享

作者&#xff1a;索增增&#xff08;小红书&#xff09;、宋泽辉&#xff08;小红书&#xff09;、张佐玮&#xff08;阿里云&#xff09; 背景介绍 Koordinator 是一个开源项目&#xff0c;基于阿里巴巴在容器调度领域多年累积的经验孵化诞生&#xff0c;目前已经支持了 K8s…

LZ码基本概念

LZ码是一种无损压缩算法&#xff0c;由Lempel和Ziv两位计算机科学家提出并命名。它是一种基于字典的压缩方法&#xff0c;可以将数据有效地压缩存储&#xff0c;同时实现高效的解压缩。 LZ码的基本概念是利用字典来存储先前遇到的字符串&#xff0c;然后用较短的代表符号来表示…